1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2012-2019 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 Ext.namespace('LABKEY', 'LABKEY.ext');
 21 
 22 /**
 23  * Constructs a new LabKey EditorGridPanel using the supplied configuration.
 24  * @class <p><font color="red">DEPRECATED</font> - Consider using
 25  * <a href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a> instead.</p>
 26  * <p>LabKey extension to the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a>,
 27  * which can provide editable grid views of data in the LabKey server. If the current user has appropriate permissions,
 28  * the user may edit data, save changes, insert new rows, or delete rows.</p>
 29  * <p>If you use any of the LabKey APIs that extend Ext APIs, you must either make your code open source or
 30  * <a href="https://www.labkey.org/Documentation/wiki-page.view?name=extDevelopment">purchase an Ext license</a>.</p>
 31  *            <p>Additional Documentation:
 32  *              <ul>
 33  *                  <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=javascriptTutorial">Tutorial: Create Applications with the JavaScript API</a></li>
 34  *              </ul>      
 35  *           </p>
 36  * @constructor
 37  * @augments Ext.grid.EditorGridPanel
 38  * @param config Configuration properties. This may contain any of the configuration properties supported
 39  * by the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a>,
 40  * plus those listed here.
 41  * @param {boolean} [config.lookups] Set to false if you do not want lookup foreign keys to be resolved to the
 42  * lookup values, and do not want dropdown lookup value pickers (default is true).
 43  * @param {integer} [config.pageSize] Defines how many rows are shown at a time in the grid (default is 20).
 44  * If the EditorGridPanel is getting its data from Ext.Store, pageSize will override the value of maxRows on Ext.Store.
 45  * @param {boolean} [config.editable] Set to true if you want the user to be able to edit, insert, or delete rows (default is false).
 46  * @param {boolean} [config.autoSave] Set to false if you do not want changes automatically saved when the user leaves the row (default is true).
 47  * @param {boolean} [config.enableFilters] True to enable user-filtering of columns (default is false)
 48  * @param {string} [config.loadingCaption] The string to display in a cell when loading the lookup values (default is "[loading...]").
 49  * @param {string} [config.lookupNullCaption] The string to display for a null value in a lookup column (default is "[none]").
 50  * @param {boolean} [config.showExportButton] Set to false to hide the Export button in the toolbar. True by default.
 51  * @example Basic Example: <pre name="code" class="xml">
 52 <script type="text/javascript">
 53     var _grid;
 54 
 55     //Use the Ext.onReady() function to define what code should
 56     //be executed once the page is fully loaded.
 57     //you must use this if you supply a renderTo config property
 58     Ext.onReady(function(){
 59         _grid = new LABKEY.ext.EditorGridPanel({
 60             store: new LABKEY.ext.Store({
 61                 schemaName: 'lists',
 62                 queryName: 'People'
 63             }),
 64             renderTo: 'grid',
 65             width: 800,
 66             autoHeight: true,
 67             title: 'Example',
 68             editable: true
 69         });
 70     });
 71 </script>
 72 <div id='grid'/> </pre>
 73  * @example Advanced Example:
 74  *
 75 This snippet shows how to link a column in an EditorGridPanel to a details/update
 76 page.  It adds a custom column renderer to the grid column model by hooking
 77 the 'columnmodelcustomize' event.  Since the column is a lookup, it is helpful to
 78 chain the base renderer so that it does the lookup magic for you.  <pre name="code" class="xml">
 79 <script type="text/javascript">
 80 var _materialTemplate;
 81 var _baseFormulationRenderer;
 82 
 83  function formulationRenderer(data, cellMetaData, record, rowIndex, colIndex, store)
 84 {
 85     return _materialTemplate.apply(record.data) + _baseFormulationRenderer(data,
 86         cellMetaData, record, rowIndex, colIndex, store) + '</a>';
 87 }
 88 
 89 function customizeColumnModel(colModel, index)
 90 {
 91     if (colModel != undefined)
 92     {
 93         var col = index['Formulation'];
 94         var url = LABKEY.ActionURL.buildURL("experiment", "showMaterial");
 95 
 96         _materialTemplate = new Ext.XTemplate('<a href="' + url +
 97             '?rowId={Formulation}">').compile();
 98         _baseFormulationRenderer = col.renderer;
 99         col.renderer = formulationRenderer;
100     }
101 }
102 
103 Ext.onReady(function(){
104     _grid = new LABKEY.ext.EditorGridPanel({
105         store: new LABKEY.ext.Store({
106             schemaName: 'lists',
107             queryName: 'FormulationExpMap'
108         }),
109         renderTo: 'gridDiv',
110         width: 600,
111         autoHeight: true,
112         title: 'Formulations to Experiments',
113         editable: true
114     });
115     _grid.on("columnmodelcustomize", customizeColumnModel);
116 });
117 </script></pre>
118  */
119 LABKEY.ext.EditorGridPanel = Ext.extend(Ext.grid.EditorGridPanel, {
120     initComponent : function() {
121 
122         Ext.QuickTips.init();
123         Ext.apply(Ext.QuickTips.getQuickTip(), {
124             dismissDelay: 15000
125         });
126 
127         //set config defaults
128         Ext.applyIf(this, {
129             lookups: true,
130             pageSize: 20,
131             editable: false,
132             enableFilters: false,
133             autoSave: true,
134             loadingCaption: "[loading...]",
135             lookupNullCaption: "[none]",
136             viewConfig: {forceFit: true},
137             id: Ext.id(undefined, "labkey-ext-grid"),
138             loadMask: true,
139             colModel: new Ext.grid.ColumnModel([]),
140             selModel: new Ext.grid.CheckboxSelectionModel({moveEditorOnEnter: false}),
141             showExportButton: true
142         });
143         this.setupDefaultPanelConfig();
144 
145         LABKEY.ext.EditorGridPanel.superclass.initComponent.apply(this, arguments);
146 
147         /**
148          * @memberOf LABKEY.ext.EditorGridPanel#
149          * @name columnmodelcustomize
150          * @event
151          * @description Use this event to customize the default column model config generated by the server.
152          * For details on the column model config, see the Ext API documentation for Ext.grid.ColumnModel
153          * (http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.ColumnModel)
154          * @param {Ext.grid.ColumnModel} columnModel The default ColumnModel config generated by the server.
155          * @param {Object} index An index map where the key is column name and the value is the entry in the column
156          * model config for that column. Since the column model config is a simple array of objects, this index helps
157          * you get to the specific columns you need to modify without doing a sequential scan.
158          */
159         /**
160          * @memberOf LABKEY.ext.EditorGridPanel#
161          * @name beforedelete
162          * @event
163          * @description Use this event to cancel the deletion of a row in the grid. If you return false
164          * from this event, the row will not be deleted
165          * @param {array} records An array of Ext.data.Record objects that the user wishes to delete
166          */
167 
168         this.addEvents("beforedelete, columnmodelcustomize");
169 
170         //subscribe to superclass events
171         this.on("beforeedit", this.onBeforeEdit, this);
172         this.on("render", this.onGridRender, this);
173 
174         //subscribe to store events and start loading it
175         if(this.store)
176         {
177             this.store.on("loadexception", this.onStoreLoadException, this);
178             this.store.on("load", this.onStoreLoad, this);
179             this.store.on("beforecommit", this.onStoreBeforeCommit, this);
180             this.store.on("commitcomplete", this.onStoreCommitComplete, this);
181             this.store.on("commitexception", this.onStoreCommitException, this);
182             this.store.load({ params : {
183                     start: 0,
184                     limit: this.pageSize,
185                     'X-LABKEY-CSRF': LABKEY.CSRF
186                 }});
187         }
188     },
189 
190     /**
191      * Returns the LABKEY.ext.Store object used to hold the
192      * lookup values for the specified column name. If the column
193      * name is not a lookup column, this method will return null.
194      * @name getLookupStore
195      * @function
196      * @memberOf LABKEY.ext.EditorGridPanel#
197      * @param {String} columnName The column name.
198      * @return {LABKEY.ext.Store} The lookup store for the given column name, or null
199      * if no lookup store exists for that column.
200      */
201     getLookupStore : function(columnName) {
202         return this.store.getLookupStore(columnName);
203     },
204 
205     /**
206      * Saves all pending changes to the database. Note that if
207      * the required fields for a given record does not have values,
208      * that record will not be saved and will remain dirty until
209      * values are supplied for all required fields.
210      * @name saveChanges
211      * @function
212      * @memberOf LABKEY.ext.EditorGridPanel#
213      */
214     saveChanges : function() {
215         this.stopEditing();
216         this.getStore().commitChanges();
217     },
218 
219     /*-- Private Methods --*/
220 
221     setupDefaultPanelConfig : function() {
222         if(!this.tbar)
223         {
224             this.tbar = [{
225                 text: 'Refresh',
226                 tooltip: 'Click to refresh the table',
227                 id: 'refresh-button',
228                 handler: this.onRefresh,
229                 scope: this
230             }];
231 
232             if(this.editable && LABKEY.user && LABKEY.user.canUpdate && !this.autoSave)
233             {
234                 this.tbar.push("-");
235                 this.tbar.push({
236                     text: 'Save Changes',
237                     tooltip: 'Click to save all changes to the database',
238                     id: 'save-button',
239                     handler: this.saveChanges,
240                     scope: this
241                 });
242             }
243 
244             if(this.editable &&LABKEY.user && LABKEY.user.canInsert)
245             {
246                 this.tbar.push("-");
247                 this.tbar.push({
248                     text: 'Add Record',
249                     tooltip: 'Click to add a row',
250                     id: 'add-record-button',
251                     handler: this.onAddRecord,
252                     scope: this
253                 });
254             }
255             if(this.editable &&LABKEY.user && LABKEY.user.canDelete)
256             {
257                 this.tbar.push("-");
258                 this.tbar.push({
259                     text: 'Delete Selected',
260                     tooltip: 'Click to delete selected row(s)',
261                     id: 'delete-records-button',
262                     handler: this.onDeleteRecords,
263                     scope: this
264                 });
265             }
266 
267             if (this.showExportButton)
268             {
269                 this.tbar.push("-");
270                 this.tbar.push({
271                     text: 'Export',
272                     tooltip: 'Click to Export the data to Excel',
273                     id: 'export-records-button',
274                     handler: function(){
275                         if (this.store)
276                             this.store.exportData("excel");
277                     },
278                     scope: this
279                 });
280             }
281         }
282 
283         if(!this.bbar)
284         {
285             this.bbar = new Ext.PagingToolbar({
286                     pageSize: this.pageSize, //default is 20
287                     store: this.store,
288                     displayInfo: true,
289                     emptyMsg: "No data to display" //display message when no records found
290                 });
291         }
292 
293         if(!this.keys)
294         {
295             this.keys = [
296                 {
297                     key: Ext.EventObject.ENTER,
298                     handler: this.onEnter,
299                     scope: this
300                 },
301                 {
302                     key: 45, //insert
303                     handler: this.onAddRecord,
304                     scope: this
305                 },
306                 {
307                     key: Ext.EventObject.ESC,
308                     handler: this.onEsc,
309                     scope: this
310                 },
311                 {
312                     key: Ext.EventObject.TAB,
313                     handler: this.onTab,
314                     scope: this
315                 },
316                 {
317                     key: Ext.EventObject.F2,
318                     handler: this.onF2,
319                     scope: this
320                 }
321             ];
322         }
323     },
324 
325     onStoreLoad : function(store, records, options) {
326         this.store.un("load", this.onStoreLoad, this);
327 
328         this.populateMetaMap();
329         this.setupColumnModel();
330     },
331 
332     onStoreLoadException : function(proxy, options, response, error) {
333         var msg = error;
334         if (!msg && response.responseText)
335         {
336             try
337             {
338                 var json = Ext.util.JSON.decode(response.responseText);
339                 if (json)
340                     msg = json.exception;
341             }
342             catch (err)
343             {}
344         }
345         if (!msg)
346             msg = "Unable to load data from the server!";
347 
348         Ext.Msg.alert("Error", msg);
349     },
350 
351     onStoreBeforeCommit : function(records, rows) {
352         //disable the refresh button so that it will animate
353         var pagingBar = this.getBottomToolbar();
354         if(pagingBar && pagingBar.loading)
355             pagingBar.loading.disable();
356         if(!this.savingMessage)
357             this.savingMessage = pagingBar.addText("Saving Changes...");
358         else
359             this.savingMessage.setVisible(true);
360     },
361 
362     onStoreCommitComplete : function() {
363         var pagingBar = this.getBottomToolbar();
364         if(pagingBar && pagingBar.loading)
365             pagingBar.loading.enable();
366         if(this.savingMessage)
367             this.savingMessage.setVisible(false);
368     },
369 
370     onStoreCommitException : function(message) {
371         var pagingBar = this.getBottomToolbar();
372         if(pagingBar && pagingBar.loading)
373             pagingBar.loading.enable();
374         if(this.savingMessage)
375             this.savingMessage.setVisible(false);
376     },
377 
378     onGridRender : function() {
379         //add the extContainer class to the view's hmenu
380         //NOTE: there is no public API to get to hmenu and colMenu
381         //so this might break in future versions of Ext. If you get
382         //a JavaScript error on these lines, look at the API docs for
383         //a method or property that returns the sort and column hide/show
384         //menus shown from the column headers
385 //        this.getView().hmenu.getEl().addClass("extContainer");
386 //        this.getView().colMenu.getEl().addClass("extContainer");
387 
388         //set up filtering
389         if (this.enableFilters)
390             this.initFilterMenu();
391 
392     },
393 
394     populateMetaMap : function() {
395         //the metaMap is a map from field name to meta data about the field
396         //the meta data contains the following properties:
397         // id, totalProperty, root, fields[]
398         // fields[] is an array of objects with the following properties
399         // name, type, lookup
400         // lookup is a nested object with the following properties
401         // schema, table, keyColumn, displayColumn
402         this.metaMap = {};
403         var fields = this.store.reader.jsonData.metaData.fields;
404         for(var idx = 0; idx < fields.length; ++idx)
405         {
406             var field = fields[idx];
407             this.metaMap[field.name] = field;
408         }
409     },
410 
411     setupColumnModel : function() {
412 
413         //set the columns property to the columnModel returned in the jsonData
414         this.columns = this.store.reader.jsonData.columnModel;
415 
416         //set the renderers and editors for the various columns
417         //build a column model index as we run the columns for the
418         //customize event
419         var colModelIndex = {};
420         var col;
421         var meta;
422         for(var idx = 0; idx < this.columns.length; ++idx)
423         {
424             col = this.columns[idx];
425             meta = this.metaMap[col.dataIndex];
426 
427             //this.editable can override col.editable
428             col.editable = this.editable && col.editable;
429 
430             //if column type is boolean, substitute an Ext.grid.CheckColumn
431             if(meta.type == "boolean" || meta.type == "bool")
432             {
433                 col = this.columns[idx] = new Ext.grid.CheckColumn(col);
434                 if(col.editable)
435                     col.init(this);
436                 col.editable = false; //check columns apply edits immediately, so we don't want to go into edit mode
437             }
438 
439             if(meta.hidden || meta.isHidden)
440                 col.hidden = true; 
441 
442             if(col.editable && !col.editor)
443                 col.editor = this.getDefaultEditor(col, meta);
444             if(!col.renderer)
445                 col.renderer = this.getDefaultRenderer(col, meta);
446 
447             //remember the first editable column (used during add record)
448             if(!this.firstEditableColumn && col.editable)
449                 this.firstEditableColumn = idx;
450 
451             //HTML-encode the column header
452             if(col.header)
453                 col.header = Ext.util.Format.htmlEncode(col.header);
454 
455             colModelIndex[col.dataIndex] = col;
456         }
457 
458         //if a sel model has been set, and if it needs to be added as a column,
459         //add it to the front of the list.
460         //CheckBoxSelectionModel needs to be added to the column model for
461         //the check boxes to show up.
462         //(not sure why its constructor doesn't do this automatically).
463         if(this.getSelectionModel() && this.getSelectionModel().renderer)
464             this.columns = [this.getSelectionModel()].concat(this.columns);
465 
466         //register for the rowdeselect event if the selmodel supports events
467         //and if autoSave is on
468         if(this.getSelectionModel().on && this.autoSave)
469             this.getSelectionModel().on("rowselect", this.onRowSelect, this);
470 
471         //add custom renderers for multiline/long-text columns
472         this.setLongTextRenderers();
473 
474         //fire the "columnmodelcustomize" event to allow clients
475         //to modify our default configuration of the column model
476         this.fireEvent("columnmodelcustomize", this.columns, colModelIndex);
477 
478         //reset the column model
479         this.reconfigure(this.store, new Ext.grid.ColumnModel(this.columns));
480     },
481 
482     getDefaultRenderer : function(col, meta) {
483         if(meta.lookup && this.lookups && col.editable) //no need to use a lookup renderer if column is not editable
484             return this.getLookupRenderer(col, meta);
485 
486         return function(data, cellMetaData, record, rowIndex, colIndex, store)
487         {
488             if(record.json && record.json[meta.name] && record.json[meta.name].mvValue)
489             {
490                 var mvValue = record.json[meta.name].mvValue;
491                 //get corresponding message from qcInfo section of JSON and set up a qtip
492                 if(store.reader.jsonData.qcInfo && store.reader.jsonData.qcInfo[mvValue])
493                 {
494                     cellMetaData.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(store.reader.jsonData.qcInfo[mvValue]) + "\"";
495                     cellMetaData.css = "labkey-mv";
496                 }
497                 return mvValue;
498             }
499 
500             if(record.json && record.json[meta.name] && record.json[meta.name].displayValue)
501                 return record.json[meta.name].displayValue;
502             
503             if(null == data || undefined == data || data.toString().length == 0)
504                 return data;
505 
506             //format data into a string
507             var displayValue;
508             switch (meta.type)
509             {
510                 case "date":
511                     var date = new Date(data);
512                     if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0)
513                         displayValue = date.format("Y-m-d");
514                     else
515                         displayValue = date.format("Y-m-d H:i:s");
516                     break;
517                 case "string":
518                 case "boolean":
519                 case "int":
520                 case "float":
521                 default:
522                     displayValue = data.toString();
523             }
524 
525             //if meta.file is true, add an <img> for the file icon
526             if(meta.file)
527             {
528                 displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(data) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue;
529                 //since the icons are 16x16, cut the default padding down to just 1px
530                 cellMetaData.attr = "style=\"padding: 1px 1px 1px 1px\"";
531             }
532 
533             //wrap in <a> if url is present in the record's original JSON
534             if(col.showLink !== false && record.json && record.json[meta.name] && record.json[meta.name].url)
535                 return "<a href=\"" + record.json[meta.name].url + "\">" + displayValue + "</a>";
536             else
537                 return displayValue;
538         };
539     },
540 
541     getLookupRenderer : function(col, meta) {
542         var lookupStore = this.store.getLookupStore(meta.name, !col.required);
543         lookupStore.on("loadexception", this.onLookupStoreError, this);
544         lookupStore.on("load", this.onLookupStoreLoad, this);
545 
546         return function(data, cellMetaData, record, rowIndex, colIndex, store)
547         {
548             if(record.json && record.json[meta.name] && record.json[meta.name].displayValue)
549                 return record.json[meta.name].displayValue;
550             
551             if(null == data || undefined == data || data.toString().length == 0)
552                 return data;
553 
554             if(lookupStore.loadError)
555                 return "ERROR: " + lookupStore.loadError.message;
556 
557             if(0 === lookupStore.getCount() && !lookupStore.isLoading)
558             {
559                 lookupStore.load();
560                 return "loading...";
561             }
562 
563             var lookupRecord = lookupStore.getById(data);
564             if (lookupRecord)
565                 return lookupRecord.data[meta.lookup.displayColumn];
566             else if (data)
567                 return "[" + data + "]";
568             else
569                 return this.lookupNullCaption || "[none]";
570         };
571     },
572 
573     onLookupStoreLoad : function(store, records, options) {
574         if(this.view && !this.activeEditor)
575             this.view.refresh();
576     },
577 
578     onLookupStoreError : function(proxy, type, action, options, response)
579     {
580         var message = "";
581         if (type == 'response')
582         {
583             var ctype = response.getResponseHeader("Content-Type");
584             if(ctype.indexOf("application/json") >= 0)
585             {
586                 var errorJson = Ext.util.JSON.decode(response.responseText);
587                 if(errorJson && errorJson.exception)
588                     message = errorJson.exception;
589             }
590         }
591         else
592         {
593             if (response && response.exception)
594             {
595                 message = response.exception;
596             }
597         }
598         Ext.Msg.alert("Load Error", "Error loading lookup data");
599 
600         if(this.view)
601             this.view.refresh();
602     },
603 
604     getDefaultEditor : function(col, meta) {
605         var editor;
606 
607         //if this column is a lookup, return the lookup editor
608         if(meta.lookup && this.lookups)
609             return this.getLookupEditor(col, meta);
610 
611         switch(meta.type)
612         {
613             case "int":
614                 editor = new Ext.form.NumberField({
615                     allowDecimals : false
616                 });
617                 break;
618             case "float":
619                 editor = new Ext.form.NumberField({
620                     allowDecimals : true
621                 });
622                 break;
623             case "date":
624                 editor = new Ext.form.DateField({
625                     format : "Y-m-d",
626                     altFormats: "Y-m-d" +
627                                 'n/j/y g:i:s a|n/j/Y g:i:s a|n/j/y G:i:s|n/j/Y G:i:s|' +
628                                 'n-j-y g:i:s a|n-j-Y g:i:s a|n-j-y G:i:s|n-j-Y G:i:s|' +
629                                 'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
630                                 'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
631                                 'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
632                                 'n/j/y|n/j/Y|' +
633                                 'n-j-y|n-j-Y|' +
634                                 'j-M-y|j-M-Y|' +
635                                 'Y-n-d H:i:s|Y-n-d|' +
636                                 'j M Y H:i:s' // 10 Sep 2009 01:24:12
637                 });
638                 //HACK: the DateMenu is created by the DateField
639                 //and there's no config on DateField that lets you specify
640                 //a CSS class to add to the DateMenu. If we create it now,
641                 //their code will just use the one we create.
642                 //See DateField.js in the Ext source
643                 editor.menu = new Ext.menu.DateMenu({cls: 'extContainer'});
644                 break;
645             case "boolean":
646                 editor = new Ext.form.Checkbox();
647                 break;
648             case "string":
649             default:
650                 editor = new Ext.form.TextField();
651                 break;
652         }
653 
654         if (editor)
655             editor.allowBlank = !col.required;
656 
657         return editor;
658     },
659 
660     getLookupEditor : function(col, meta) {
661         var store = this.store.getLookupStore(meta.name, !col.required);
662         return new Ext.form.ComboBox({
663             store: store,
664             allowBlank: !col.required,
665             typeAhead: false,
666             triggerAction: 'all',
667             editable: false,
668             displayField: meta.lookup.displayColumn,
669             valueField: meta.lookup.keyColumn,
670             tpl : '<tpl for="."><div class="x-combo-list-item">{[values["' + meta.lookup.displayColumn + '"]]}</div></tpl>', //FIX: 5860
671             listClass: 'labkey-grid-editor'
672         });
673     },
674 
675     setLongTextRenderers : function() {
676         var col;
677         for(var idx = 0; idx < this.columns.length; ++idx)
678         {
679             col = this.columns[idx];
680             if(col.multiline || (undefined === col.multiline && col.scale > 255 && this.metaMap[col.dataIndex].type === "string"))
681             {
682                 col.renderer = function(data, metadata, record, rowIndex, colIndex, store)
683                 {
684                     //set quick-tip attributes and let Ext QuickTips do the work
685                     metadata.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(data) + "\"";
686                     return data;
687                 };
688 
689                 if(col.editable)
690                     col.editor = new LABKEY.ext.LongTextField({
691                         columnName: col.dataIndex
692                     });
693             }
694         }
695     },
696 
697     onRefresh : function() {
698         this.getStore().reload();
699     },
700 
701     onAddRecord : function() {
702         if(!this.store || !this.store.addRecord)
703             return;
704 
705         this.stopEditing();
706         this.store.addRecord({}, 0); //add a blank record in the first position
707         this.getSelectionModel().selectFirstRow();
708         this.startEditing(0, this.firstEditableColumn);
709     },
710 
711     onDeleteRecords : function() {
712         var records = this.getSelectionModel().getSelections();
713         if (records && records.length)
714         {
715             if(this.fireEvent("beforedelete", {records: records}))
716             {
717                 Ext.Msg.show({
718                     title: "Confirm Delete",
719                     msg: records.length > 1
720                             ? "Are you sure you want to delete the "
721                                 + records.length + " selected records? This cannot be undone."
722                             : "Are you sure you want to delete the selected record? This cannot be undone.",
723                     icon: Ext.MessageBox.QUESTION,
724                     buttons: {ok: "Delete", cancel: "Cancel"},
725                     scope: this,
726                     fn: function(buttonId) {
727                         if(buttonId == "ok")
728                             this.store.deleteRecords(records);
729                     }
730                 });
731             }
732         }
733     },
734 
735     onRowSelect : function(selmodel, rowIndex) {
736         if(this.autoSave)
737             this.saveChanges();
738     },
739 
740     onBeforeEdit : function(evt) {
741         if(this.getStore().isUpdateInProgress(evt.record))
742             return false;
743 
744         if(!this.getSelectionModel().isSelected(evt.row))
745             this.getSelectionModel().selectRow(evt.row);
746 
747         var editor = this.getColumnModel().getCellEditor(evt.column, evt.row);
748         var displayValue = (evt.record.json && evt.record.json[evt.field]) ? evt.record.json[evt.field].displayValue : undefined;
749 
750         //set the value not found text to be the display value if there is one
751         if(editor && editor.field && editor.field.displayField && displayValue)
752             editor.field.valueNotFoundText = displayValue;
753 
754         //reset combo mode to local if the lookup store is already populated
755         if(editor && editor.field && editor.field.displayField && editor.field.store && editor.field.store.getCount() > 0)
756             editor.field.mode = "local";
757     },
758 
759     onEnter : function() {
760         this.stopEditing();
761 
762         //move selection down to the next row, or commit if on last row
763         var selmodel = this.getSelectionModel();
764         if(selmodel.hasNext())
765             selmodel.selectNext();
766         else if(this.autoSave)
767             this.saveChanges();
768     },
769 
770     onEsc : function() {
771         //if the currently selected record is dirty,
772         //reject the edits
773         var record = this.getSelectionModel().getSelected();
774         if(record && record.dirty)
775         {
776             if(record.isNew)
777                 this.getStore().remove(record);
778             else
779                 record.reject();
780         }
781     },
782 
783     onTab : function() {
784         if(this.autoSave)
785             this.saveChanges();
786     },
787 
788     onF2 : function() {
789         var record = this.getSelectionModel().getSelected();
790         if(record)
791         {
792             var index = this.getStore().findBy(function(recordComp, id){return id == record.id;});
793             if(index >= 0 && undefined !== this.firstEditableColumn)
794                 this.startEditing(index, this.firstEditableColumn);
795         }
796 
797     },
798 
799     initFilterMenu : function()
800     {
801         var filterItem = new Ext.menu.Item({text:"Filter...", scope:this, handler:function() {this.handleFilter();}});
802         var hmenu = this.getView().hmenu;
803 //        hmenu.getEl().addClass("extContainer");
804         hmenu.addItem(filterItem);
805     },
806 
807     handleFilter :function ()
808     {
809         var view = this.getView();
810         var col = view.cm.config[view.hdCtxIndex];
811 
812         this.showFilterWindow(col);
813     },
814 
815     showFilterWindow: function(col)
816     {
817         var colName = col.dataIndex;
818         var meta = this.getStore().findFieldMeta(colName);
819         var grid = this; //Stash for later use in callbacks.
820 
821         var filterColName = meta.lookup ? colName + "/" + meta.lookup.displayColumn : colName;
822         var filterColType;
823         if (meta.lookup)
824         {
825             var lookupStore = this.store.getLookupStore(filterColName);
826             if (null != lookupStore)
827             {
828                 meta = lookupStore.findFieldMeta(meta.lookup.displayColumn);
829                 filterColType = meta ? meta.type : "string";
830             }
831             else
832                 filterColType = "string";
833         }
834         else
835             filterColType = meta.type;
836 
837         var colFilters = this.getColumnFilters(colName);
838         var dropDowns = [
839             LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 1 ? colFilters[0].getFilterType().getURLSuffix() : null, true),
840             LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 2 ? colFilters[1].getFilterType().getURLSuffix() : null)];
841         var valueEditors = [
842             new Ext.form.TextField({value:colFilters.length > 0 ? colFilters[0].getValue() : "",width:250}),
843             new Ext.form.TextField({value:colFilters.length > 1 ? colFilters[1].getValue() : "",width:250, hidden:colFilters.length < 2, hideMode:'visibility'})];
844 
845         dropDowns[0].valueEditor = valueEditors[0];
846         dropDowns[1].valueEditor = valueEditors[1];
847 
848         function validateEntry(index)
849         {
850             var filterType = dropDowns[index].getFilterType();
851             return filterType.validate(valueEditors[index].getValue(), filterColType, colName);
852         }
853 
854         var win = new Ext.Window({
855             title:"Show Rows Where " + colName,
856             width:400,
857             autoHeight:true,
858             modal:true,
859             items:[dropDowns[0], valueEditors[0], new Ext.form.Label({text:" and"}),
860                     dropDowns[1], valueEditors[1]],
861             //layout:'column',
862             buttons:[
863                 {
864                     text:"OK",
865                     handler:function() {
866                         var filters = [];
867                         var value;
868                         value = validateEntry(0);
869                         if (!value)
870                             return;
871 
872                         var filterType = dropDowns[0].getFilterType();
873                         filters.push(LABKEY.Filter.create(filterColName, value, filterType));
874                         filterType = dropDowns[1].getFilterType();
875                         if (filterType && filterType.getURLSuffix().length > 0)
876                         {
877                             value = validateEntry(1);
878                             if (!value)
879                                 return;
880                             filters.push(LABKEY.Filter.create(filterColName, value, filterType));
881                         }
882                         grid.setColumnFilters(colName, filters);
883                         win.close();
884                     }
885                 },
886                 {
887                     text:"Cancel",
888                     handler:function() {win.close();}
889                 },
890                 {
891                     text:"Clear Filter",
892                     handler:function() {grid.setColumnFilters(colName, []); win.close();}
893                 },
894                 {
895                     text:"Clear All Filters",
896                     handler:function() {grid.getStore().setUserFilters([]); grid.getStore().load({params:{start:0, limit:grid.pageSize}}); win.close()}
897                 }
898             ]
899         });
900         win.show();
901         //Focus doesn't work right away (who knows why?) so defer it...
902         function f() {valueEditors[0].focus();};
903         f.defer(100);
904     },
905 
906     getColumnFilters: function(colName)
907     {
908         var colFilters = [];
909         Ext.each(this.getStore().getUserFilters(), function(filter) {
910             if (filter.getColumnName() == colName)
911                 colFilters.push(filter);
912         });
913         return colFilters;
914     },
915 
916     setColumnFilters: function(colName, filters)
917     {
918         var newFilters = [];
919         Ext.each(this.getStore().getUserFilters(), function(filter) {
920             if (filter.getColumnName() != colName)
921                 newFilters.push(filter);
922         });
923         if (filters)
924             Ext.each(filters, function(filter) {newFilters.push(filter);});
925 
926         this.getStore().setUserFilters(newFilters);
927         this.getStore().load({params:{start:0, limit:this.pageSize}});
928     }
929 });
930 
931 LABKEY.ext.EditorGridPanel.createFilterCombo = function (type, filterOp, first)
932 {
933     var ft = LABKEY.Filter.Types;
934     var defaultFilterTypes = {
935         "int":ft.EQUAL, "string":ft.STARTS_WITH, "boolean":ft.EQUAL, "float":ft.GTE,  "date":ft.DATE_EQUAL
936     };
937 
938     //Option lists for drop-downs. Filled in on-demand based on filter type
939     var dropDownOptions = [];
940     Ext.each(LABKEY.Filter.getFilterTypesForType(type), function (filterType) {
941         dropDownOptions.push([filterType.getURLSuffix(), filterType.getDisplayText()]);
942     });
943 
944     //Do the ext magic for the options. Gets easier in ext 2.2
945     var options = (!first) ? [['', 'no other filter']].concat(dropDownOptions) : dropDownOptions;
946     var store = new Ext.data.SimpleStore({'id': 0, fields: ['value', 'text'], data: options });
947     var combo = new Ext.form.ComboBox({
948         store:store,
949         forceSelection:true,
950         valueField:'value',
951         displayField:'text',
952         mode:'local',
953         allowBlank:false,
954         triggerAction:'all',
955         value:filterOp ? filterOp : ((!first) ? '' : defaultFilterTypes[type].getURLSuffix())
956     });
957     combo.on("select", function(combo, record, itemNo) {
958         var filter = this.getFilterType();
959         if (this.valueEditor)
960             this.valueEditor.setVisible(filter != null && filter.isDataValueRequired());
961     });
962 
963     combo.getFilterType = function () {
964         return LABKEY.Filter.getFilterTypeForURLSuffix(this.getValue());
965     };
966 
967     return combo;
968 };
969 
970 
971 // Check column plugin
972 Ext.grid.CheckColumn = function(config){
973     Ext.apply(this, config);
974     if(!this.id){
975         this.id = Ext.id();
976     }
977     this.renderer = this.renderer.createDelegate(this);
978 };
979 
980 Ext.grid.CheckColumn.prototype ={
981     init : function(grid){
982         this.grid = grid;
983         if(grid.getView() && grid.getView().mainBody)
984         {
985             grid.getView().mainBody.on('mousedown', this.onMouseDown, this);
986         }
987         else
988         {
989             this.grid.on('render', function(){
990                 var view = this.grid.getView();
991                 view.mainBody.on('mousedown', this.onMouseDown, this);
992             }, this);
993         }
994     },
995 
996     onMouseDown : function(e, t){
997         if(t.className && t.className.indexOf('x-grid3-cc-'+this.id) != -1){
998             e.stopEvent();
999             var index = this.grid.getView().findRowIndex(t);
1000             var record = this.grid.store.getAt(index);
1001             this.grid.getSelectionModel().selectRow(index);
1002             record.set(this.dataIndex, !record.data[this.dataIndex]);
1003         }
1004     },
1005 
1006     renderer : function(v, p, record){
1007         p.css += ' x-grid3-check-col-td';
1008         return '<div class="x-grid3-check-col'+(v?'-on':'')+' x-grid3-cc-'+this.id+'"> </div>';
1009     }
1010 };
1011