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) 2008-2016 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/wiki/home/Documentation/page.view?name=extDevelopment">purchase an Ext license</a>.</p>
 31  *            <p>Additional Documentation:
 32  *              <ul>
 33  *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/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                 }});
186         }
187     },
188 
189     /**
190      * Returns the LABKEY.ext.Store object used to hold the
191      * lookup values for the specified column name. If the column
192      * name is not a lookup column, this method will return null.
193      * @name getLookupStore
194      * @function
195      * @memberOf LABKEY.ext.EditorGridPanel#
196      * @param {String} columnName The column name.
197      * @return {LABKEY.ext.Store} The lookup store for the given column name, or null
198      * if no lookup store exists for that column.
199      */
200     getLookupStore : function(columnName) {
201         return this.store.getLookupStore(columnName);
202     },
203 
204     /**
205      * Saves all pending changes to the database. Note that if
206      * the required fields for a given record does not have values,
207      * that record will not be saved and will remain dirty until
208      * values are supplied for all required fields.
209      * @name saveChanges
210      * @function
211      * @memberOf LABKEY.ext.EditorGridPanel#
212      */
213     saveChanges : function() {
214         this.stopEditing();
215         this.getStore().commitChanges();
216     },
217 
218     /*-- Private Methods --*/
219 
220     setupDefaultPanelConfig : function() {
221         if(!this.tbar)
222         {
223             this.tbar = [{
224                 text: 'Refresh',
225                 tooltip: 'Click to refresh the table',
226                 id: 'refresh-button',
227                 handler: this.onRefresh,
228                 scope: this
229             }];
230 
231             if(this.editable && LABKEY.user && LABKEY.user.canUpdate && !this.autoSave)
232             {
233                 this.tbar.push("-");
234                 this.tbar.push({
235                     text: 'Save Changes',
236                     tooltip: 'Click to save all changes to the database',
237                     id: 'save-button',
238                     handler: this.saveChanges,
239                     scope: this
240                 });
241             }
242 
243             if(this.editable &&LABKEY.user && LABKEY.user.canInsert)
244             {
245                 this.tbar.push("-");
246                 this.tbar.push({
247                     text: 'Add Record',
248                     tooltip: 'Click to add a row',
249                     id: 'add-record-button',
250                     handler: this.onAddRecord,
251                     scope: this
252                 });
253             }
254             if(this.editable &&LABKEY.user && LABKEY.user.canDelete)
255             {
256                 this.tbar.push("-");
257                 this.tbar.push({
258                     text: 'Delete Selected',
259                     tooltip: 'Click to delete selected row(s)',
260                     id: 'delete-records-button',
261                     handler: this.onDeleteRecords,
262                     scope: this
263                 });
264             }
265 
266             if (this.showExportButton)
267             {
268                 this.tbar.push("-");
269                 this.tbar.push({
270                     text: 'Export',
271                     tooltip: 'Click to Export the data to Excel',
272                     id: 'export-records-button',
273                     handler: function(){
274                         if (this.store)
275                             this.store.exportData("excel");
276                     },
277                     scope: this
278                 });
279             }
280         }
281 
282         if(!this.bbar)
283         {
284             this.bbar = new Ext.PagingToolbar({
285                     pageSize: this.pageSize, //default is 20
286                     store: this.store,
287                     displayInfo: true,
288                     emptyMsg: "No data to display" //display message when no records found
289                 });
290         }
291 
292         if(!this.keys)
293         {
294             this.keys = [
295                 {
296                     key: Ext.EventObject.ENTER,
297                     handler: this.onEnter,
298                     scope: this
299                 },
300                 {
301                     key: 45, //insert
302                     handler: this.onAddRecord,
303                     scope: this
304                 },
305                 {
306                     key: Ext.EventObject.ESC,
307                     handler: this.onEsc,
308                     scope: this
309                 },
310                 {
311                     key: Ext.EventObject.TAB,
312                     handler: this.onTab,
313                     scope: this
314                 },
315                 {
316                     key: Ext.EventObject.F2,
317                     handler: this.onF2,
318                     scope: this
319                 }
320             ];
321         }
322     },
323 
324     onStoreLoad : function(store, records, options) {
325         this.store.un("load", this.onStoreLoad, this);
326 
327         this.populateMetaMap();
328         this.setupColumnModel();
329     },
330 
331     onStoreLoadException : function(proxy, options, response, error) {
332         var msg = error;
333         if(!msg && response.responseText)
334         {
335             var json = Ext.util.JSON.decode(response.responseText);
336             if(json)
337                 msg = json.exception;
338         }
339         if(!msg)
340             msg = "Unable to load data from the server!";
341 
342         Ext.Msg.alert("Error", msg);
343     },
344 
345     onStoreBeforeCommit : function(records, rows) {
346         //disable the refresh button so that it will animate
347         var pagingBar = this.getBottomToolbar();
348         if(pagingBar && pagingBar.loading)
349             pagingBar.loading.disable();
350         if(!this.savingMessage)
351             this.savingMessage = pagingBar.addText("Saving Changes...");
352         else
353             this.savingMessage.setVisible(true);
354     },
355 
356     onStoreCommitComplete : function() {
357         var pagingBar = this.getBottomToolbar();
358         if(pagingBar && pagingBar.loading)
359             pagingBar.loading.enable();
360         if(this.savingMessage)
361             this.savingMessage.setVisible(false);
362     },
363 
364     onStoreCommitException : function(message) {
365         var pagingBar = this.getBottomToolbar();
366         if(pagingBar && pagingBar.loading)
367             pagingBar.loading.enable();
368         if(this.savingMessage)
369             this.savingMessage.setVisible(false);
370     },
371 
372     onGridRender : function() {
373         //add the extContainer class to the view's hmenu
374         //NOTE: there is no public API to get to hmenu and colMenu
375         //so this might break in future versions of Ext. If you get
376         //a JavaScript error on these lines, look at the API docs for
377         //a method or property that returns the sort and column hide/show
378         //menus shown from the column headers
379 //        this.getView().hmenu.getEl().addClass("extContainer");
380 //        this.getView().colMenu.getEl().addClass("extContainer");
381 
382         //set up filtering
383         if (this.enableFilters)
384             this.initFilterMenu();
385 
386     },
387 
388     populateMetaMap : function() {
389         //the metaMap is a map from field name to meta data about the field
390         //the meta data contains the following properties:
391         // id, totalProperty, root, fields[]
392         // fields[] is an array of objects with the following properties
393         // name, type, lookup
394         // lookup is a nested object with the following properties
395         // schema, table, keyColumn, displayColumn
396         this.metaMap = {};
397         var fields = this.store.reader.jsonData.metaData.fields;
398         for(var idx = 0; idx < fields.length; ++idx)
399         {
400             var field = fields[idx];
401             this.metaMap[field.name] = field;
402         }
403     },
404 
405     setupColumnModel : function() {
406 
407         //set the columns property to the columnModel returned in the jsonData
408         this.columns = this.store.reader.jsonData.columnModel;
409 
410         //set the renderers and editors for the various columns
411         //build a column model index as we run the columns for the
412         //customize event
413         var colModelIndex = {};
414         var col;
415         var meta;
416         for(var idx = 0; idx < this.columns.length; ++idx)
417         {
418             col = this.columns[idx];
419             meta = this.metaMap[col.dataIndex];
420 
421             //this.editable can override col.editable
422             col.editable = this.editable && col.editable;
423 
424             //if column type is boolean, substitute an Ext.grid.CheckColumn
425             if(meta.type == "boolean" || meta.type == "bool")
426             {
427                 col = this.columns[idx] = new Ext.grid.CheckColumn(col);
428                 if(col.editable)
429                     col.init(this);
430                 col.editable = false; //check columns apply edits immediately, so we don't want to go into edit mode
431             }
432 
433             if(meta.hidden || meta.isHidden)
434                 col.hidden = true; 
435 
436             if(col.editable && !col.editor)
437                 col.editor = this.getDefaultEditor(col, meta);
438             if(!col.renderer)
439                 col.renderer = this.getDefaultRenderer(col, meta);
440 
441             //remember the first editable column (used during add record)
442             if(!this.firstEditableColumn && col.editable)
443                 this.firstEditableColumn = idx;
444 
445             //HTML-encode the column header
446             if(col.header)
447                 col.header = Ext.util.Format.htmlEncode(col.header);
448 
449             colModelIndex[col.dataIndex] = col;
450         }
451 
452         //if a sel model has been set, and if it needs to be added as a column,
453         //add it to the front of the list.
454         //CheckBoxSelectionModel needs to be added to the column model for
455         //the check boxes to show up.
456         //(not sure why its constructor doesn't do this automatically).
457         if(this.getSelectionModel() && this.getSelectionModel().renderer)
458             this.columns = [this.getSelectionModel()].concat(this.columns);
459 
460         //register for the rowdeselect event if the selmodel supports events
461         //and if autoSave is on
462         if(this.getSelectionModel().on && this.autoSave)
463             this.getSelectionModel().on("rowselect", this.onRowSelect, this);
464 
465         //add custom renderers for multiline/long-text columns
466         this.setLongTextRenderers();
467 
468         //fire the "columnmodelcustomize" event to allow clients
469         //to modify our default configuration of the column model
470         this.fireEvent("columnmodelcustomize", this.columns, colModelIndex);
471 
472         //reset the column model
473         this.reconfigure(this.store, new Ext.grid.ColumnModel(this.columns));
474     },
475 
476     getDefaultRenderer : function(col, meta) {
477         if(meta.lookup && this.lookups && col.editable) //no need to use a lookup renderer if column is not editable
478             return this.getLookupRenderer(col, meta);
479 
480         return function(data, cellMetaData, record, rowIndex, colIndex, store)
481         {
482             if(record.json && record.json[meta.name] && record.json[meta.name].mvValue)
483             {
484                 var mvValue = record.json[meta.name].mvValue;
485                 //get corresponding message from qcInfo section of JSON and set up a qtip
486                 if(store.reader.jsonData.qcInfo && store.reader.jsonData.qcInfo[mvValue])
487                 {
488                     cellMetaData.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(store.reader.jsonData.qcInfo[mvValue]) + "\"";
489                     cellMetaData.css = "labkey-mv";
490                 }
491                 return mvValue;
492             }
493 
494             if(record.json && record.json[meta.name] && record.json[meta.name].displayValue)
495                 return record.json[meta.name].displayValue;
496             
497             if(null == data || undefined == data || data.toString().length == 0)
498                 return data;
499 
500             //format data into a string
501             var displayValue;
502             switch (meta.type)
503             {
504                 case "date":
505                     var date = new Date(data);
506                     if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0)
507                         displayValue = date.format("Y-m-d");
508                     else
509                         displayValue = date.format("Y-m-d H:i:s");
510                     break;
511                 case "string":
512                 case "boolean":
513                 case "int":
514                 case "float":
515                 default:
516                     displayValue = data.toString();
517             }
518 
519             //if meta.file is true, add an <img> for the file icon
520             if(meta.file)
521             {
522                 displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(data) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue;
523                 //since the icons are 16x16, cut the default padding down to just 1px
524                 cellMetaData.attr = "style=\"padding: 1px 1px 1px 1px\"";
525             }
526 
527             //wrap in <a> if url is present in the record's original JSON
528             if(col.showLink !== false && record.json && record.json[meta.name] && record.json[meta.name].url)
529                 return "<a href=\"" + record.json[meta.name].url + "\">" + displayValue + "</a>";
530             else
531                 return displayValue;
532         };
533     },
534 
535     getLookupRenderer : function(col, meta) {
536         var lookupStore = this.store.getLookupStore(meta.name, !col.required);
537         lookupStore.on("loadexception", this.onLookupStoreError, this);
538         lookupStore.on("load", this.onLookupStoreLoad, this);
539 
540         return function(data, cellMetaData, record, rowIndex, colIndex, store)
541         {
542             if(record.json && record.json[meta.name] && record.json[meta.name].displayValue)
543                 return record.json[meta.name].displayValue;
544             
545             if(null == data || undefined == data || data.toString().length == 0)
546                 return data;
547 
548             if(lookupStore.loadError)
549                 return "ERROR: " + lookupStore.loadError.message;
550 
551             if(0 === lookupStore.getCount() && !lookupStore.isLoading)
552             {
553                 lookupStore.load();
554                 return "loading...";
555             }
556 
557             var lookupRecord = lookupStore.getById(data);
558             if (lookupRecord)
559                 return lookupRecord.data[meta.lookup.displayColumn];
560             else if (data)
561                 return "[" + data + "]";
562             else
563                 return this.lookupNullCaption || "[none]";
564         };
565     },
566 
567     onLookupStoreLoad : function(store, records, options) {
568         if(this.view && !this.activeEditor)
569             this.view.refresh();
570     },
571 
572     onLookupStoreError : function(proxy, type, action, options, response)
573     {
574         var message = "";
575         if (type == 'response')
576         {
577             var ctype = response.getResponseHeader("Content-Type");
578             if(ctype.indexOf("application/json") >= 0)
579             {
580                 var errorJson = Ext.util.JSON.decode(response.responseText);
581                 if(errorJson && errorJson.exception)
582                     message = errorJson.exception;
583             }
584         }
585         else
586         {
587             if (response && response.exception)
588             {
589                 message = response.exception;
590             }
591         }
592         Ext.Msg.alert("Load Error", "Error loading lookup data");
593 
594         if(this.view)
595             this.view.refresh();
596     },
597 
598     getDefaultEditor : function(col, meta) {
599         var editor;
600 
601         //if this column is a lookup, return the lookup editor
602         if(meta.lookup && this.lookups)
603             return this.getLookupEditor(col, meta);
604 
605         switch(meta.type)
606         {
607             case "int":
608                 editor = new Ext.form.NumberField({
609                     allowDecimals : false
610                 });
611                 break;
612             case "float":
613                 editor = new Ext.form.NumberField({
614                     allowDecimals : true
615                 });
616                 break;
617             case "date":
618                 editor = new Ext.form.DateField({
619                     format : "Y-m-d",
620                     altFormats: "Y-m-d" +
621                                 '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|' +
622                                 '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|' +
623                                 'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
624                                 'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
625                                 'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
626                                 'n/j/y|n/j/Y|' +
627                                 'n-j-y|n-j-Y|' +
628                                 'j-M-y|j-M-Y|' +
629                                 'Y-n-d H:i:s|Y-n-d|' +
630                                 'j M Y H:i:s' // 10 Sep 2009 01:24:12
631                 });
632                 //HACK: the DateMenu is created by the DateField
633                 //and there's no config on DateField that lets you specify
634                 //a CSS class to add to the DateMenu. If we create it now,
635                 //their code will just use the one we create.
636                 //See DateField.js in the Ext source
637                 editor.menu = new Ext.menu.DateMenu({cls: 'extContainer'});
638                 break;
639             case "boolean":
640                 editor = new Ext.form.Checkbox();
641                 break;
642             case "string":
643             default:
644                 editor = new Ext.form.TextField();
645                 break;
646         }
647 
648         if (editor)
649             editor.allowBlank = !col.required;
650 
651         return editor;
652     },
653 
654     getLookupEditor : function(col, meta) {
655         var store = this.store.getLookupStore(meta.name, !col.required);
656         return new Ext.form.ComboBox({
657             store: store,
658             allowBlank: !col.required,
659             typeAhead: false,
660             triggerAction: 'all',
661             editable: false,
662             displayField: meta.lookup.displayColumn,
663             valueField: meta.lookup.keyColumn,
664             tpl : '<tpl for="."><div class="x-combo-list-item">{[values["' + meta.lookup.displayColumn + '"]]}</div></tpl>', //FIX: 5860
665             listClass: 'labkey-grid-editor'
666         });
667     },
668 
669     setLongTextRenderers : function() {
670         var col;
671         for(var idx = 0; idx < this.columns.length; ++idx)
672         {
673             col = this.columns[idx];
674             if(col.multiline || (undefined === col.multiline && col.scale > 255 && this.metaMap[col.dataIndex].type === "string"))
675             {
676                 col.renderer = function(data, metadata, record, rowIndex, colIndex, store)
677                 {
678                     //set quick-tip attributes and let Ext QuickTips do the work
679                     metadata.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(data) + "\"";
680                     return data;
681                 };
682 
683                 if(col.editable)
684                     col.editor = new LABKEY.ext.LongTextField({
685                         columnName: col.dataIndex
686                     });
687             }
688         }
689     },
690 
691     onRefresh : function() {
692         this.getStore().reload();
693     },
694 
695     onAddRecord : function() {
696         if(!this.store || !this.store.addRecord)
697             return;
698 
699         this.stopEditing();
700         this.store.addRecord({}, 0); //add a blank record in the first position
701         this.getSelectionModel().selectFirstRow();
702         this.startEditing(0, this.firstEditableColumn);
703     },
704 
705     onDeleteRecords : function() {
706         var records = this.getSelectionModel().getSelections();
707         if (records && records.length)
708         {
709             if(this.fireEvent("beforedelete", {records: records}))
710             {
711                 Ext.Msg.show({
712                     title: "Confirm Delete",
713                     msg: records.length > 1
714                             ? "Are you sure you want to delete the "
715                                 + records.length + " selected records? This cannot be undone."
716                             : "Are you sure you want to delete the selected record? This cannot be undone.",
717                     icon: Ext.MessageBox.QUESTION,
718                     buttons: {ok: "Delete", cancel: "Cancel"},
719                     scope: this,
720                     fn: function(buttonId) {
721                         if(buttonId == "ok")
722                             this.store.deleteRecords(records);
723                     }
724                 });
725             }
726         }
727     },
728 
729     onRowSelect : function(selmodel, rowIndex) {
730         if(this.autoSave)
731             this.saveChanges();
732     },
733 
734     onBeforeEdit : function(evt) {
735         if(this.getStore().isUpdateInProgress(evt.record))
736             return false;
737 
738         if(!this.getSelectionModel().isSelected(evt.row))
739             this.getSelectionModel().selectRow(evt.row);
740 
741         var editor = this.getColumnModel().getCellEditor(evt.column, evt.row);
742         var displayValue = (evt.record.json && evt.record.json[evt.field]) ? evt.record.json[evt.field].displayValue : undefined;
743 
744         //set the value not found text to be the display value if there is one
745         if(editor && editor.field && editor.field.displayField && displayValue)
746             editor.field.valueNotFoundText = displayValue;
747 
748         //reset combo mode to local if the lookup store is already populated
749         if(editor && editor.field && editor.field.displayField && editor.field.store && editor.field.store.getCount() > 0)
750             editor.field.mode = "local";
751     },
752 
753     onEnter : function() {
754         this.stopEditing();
755 
756         //move selection down to the next row, or commit if on last row
757         var selmodel = this.getSelectionModel();
758         if(selmodel.hasNext())
759             selmodel.selectNext();
760         else if(this.autoSave)
761             this.saveChanges();
762     },
763 
764     onEsc : function() {
765         //if the currently selected record is dirty,
766         //reject the edits
767         var record = this.getSelectionModel().getSelected();
768         if(record && record.dirty)
769         {
770             if(record.isNew)
771                 this.getStore().remove(record);
772             else
773                 record.reject();
774         }
775     },
776 
777     onTab : function() {
778         if(this.autoSave)
779             this.saveChanges();
780     },
781 
782     onF2 : function() {
783         var record = this.getSelectionModel().getSelected();
784         if(record)
785         {
786             var index = this.getStore().findBy(function(recordComp, id){return id == record.id;});
787             if(index >= 0 && undefined !== this.firstEditableColumn)
788                 this.startEditing(index, this.firstEditableColumn);
789         }
790 
791     },
792 
793     initFilterMenu : function()
794     {
795         var filterItem = new Ext.menu.Item({text:"Filter...", scope:this, handler:function() {this.handleFilter();}});
796         var hmenu = this.getView().hmenu;
797 //        hmenu.getEl().addClass("extContainer");
798         hmenu.addItem(filterItem);
799     },
800 
801     handleFilter :function ()
802     {
803         var view = this.getView();
804         var col = view.cm.config[view.hdCtxIndex];
805 
806         this.showFilterWindow(col);
807     },
808 
809     showFilterWindow: function(col)
810     {
811         var colName = col.dataIndex;
812         var meta = this.getStore().findFieldMeta(colName);
813         var grid = this; //Stash for later use in callbacks.
814 
815         var filterColName = meta.lookup ? colName + "/" + meta.lookup.displayColumn : colName;
816         var filterColType;
817         if (meta.lookup)
818         {
819             var lookupStore = this.store.getLookupStore(filterColName);
820             if (null != lookupStore)
821             {
822                 meta = lookupStore.findFieldMeta(meta.lookup.displayColumn);
823                 filterColType = meta ? meta.type : "string";
824             }
825             else
826                 filterColType = "string";
827         }
828         else
829             filterColType = meta.type;
830 
831         var colFilters = this.getColumnFilters(colName);
832         var dropDowns = [
833             LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 1 ? colFilters[0].getFilterType().getURLSuffix() : null, true),
834             LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 2 ? colFilters[1].getFilterType().getURLSuffix() : null)];
835         var valueEditors = [
836             new Ext.form.TextField({value:colFilters.length > 0 ? colFilters[0].getValue() : "",width:250}),
837             new Ext.form.TextField({value:colFilters.length > 1 ? colFilters[1].getValue() : "",width:250, hidden:colFilters.length < 2, hideMode:'visibility'})];
838 
839         dropDowns[0].valueEditor = valueEditors[0];
840         dropDowns[1].valueEditor = valueEditors[1];
841 
842         function validateEntry(index)
843         {
844             var filterType = dropDowns[index].getFilterType();
845             return filterType.validate(valueEditors[index].getValue(), filterColType, colName);
846         }
847 
848         var win = new Ext.Window({
849             title:"Show Rows Where " + colName,
850             width:400,
851             autoHeight:true,
852             modal:true,
853             items:[dropDowns[0], valueEditors[0], new Ext.form.Label({text:" and"}),
854                     dropDowns[1], valueEditors[1]],
855             //layout:'column',
856             buttons:[
857                 {
858                     text:"OK",
859                     handler:function() {
860                         var filters = [];
861                         var value;
862                         value = validateEntry(0);
863                         if (!value)
864                             return;
865 
866                         var filterType = dropDowns[0].getFilterType();
867                         filters.push(LABKEY.Filter.create(filterColName, value, filterType));
868                         filterType = dropDowns[1].getFilterType();
869                         if (filterType && filterType.getURLSuffix().length > 0)
870                         {
871                             value = validateEntry(1);
872                             if (!value)
873                                 return;
874                             filters.push(LABKEY.Filter.create(filterColName, value, filterType));
875                         }
876                         grid.setColumnFilters(colName, filters);
877                         win.close();
878                     }
879                 },
880                 {
881                     text:"Cancel",
882                     handler:function() {win.close();}
883                 },
884                 {
885                     text:"Clear Filter",
886                     handler:function() {grid.setColumnFilters(colName, []); win.close();}
887                 },
888                 {
889                     text:"Clear All Filters",
890                     handler:function() {grid.getStore().setUserFilters([]); grid.getStore().load({params:{start:0, limit:grid.pageSize}}); win.close()}
891                 }
892             ]
893         });
894         win.show();
895         //Focus doesn't work right away (who knows why?) so defer it...
896         function f() {valueEditors[0].focus();};
897         f.defer(100);
898     },
899 
900     getColumnFilters: function(colName)
901     {
902         var colFilters = [];
903         Ext.each(this.getStore().getUserFilters(), function(filter) {
904             if (filter.getColumnName() == colName)
905                 colFilters.push(filter);
906         });
907         return colFilters;
908     },
909 
910     setColumnFilters: function(colName, filters)
911     {
912         var newFilters = [];
913         Ext.each(this.getStore().getUserFilters(), function(filter) {
914             if (filter.getColumnName() != colName)
915                 newFilters.push(filter);
916         });
917         if (filters)
918             Ext.each(filters, function(filter) {newFilters.push(filter);});
919 
920         this.getStore().setUserFilters(newFilters);
921         this.getStore().load({params:{start:0, limit:this.pageSize}});
922     }
923 });
924 
925 LABKEY.ext.EditorGridPanel.createFilterCombo = function (type, filterOp, first)
926 {
927     var ft = LABKEY.Filter.Types;
928     var defaultFilterTypes = {
929         "int":ft.EQUAL, "string":ft.STARTS_WITH, "boolean":ft.EQUAL, "float":ft.GTE,  "date":ft.DATE_EQUAL
930     };
931 
932     //Option lists for drop-downs. Filled in on-demand based on filter type
933     var dropDownOptions = [];
934     Ext.each(LABKEY.Filter.getFilterTypesForType(type), function (filterType) {
935         dropDownOptions.push([filterType.getURLSuffix(), filterType.getDisplayText()]);
936     });
937 
938     //Do the ext magic for the options. Gets easier in ext 2.2
939     var options = (!first) ? [['', 'no other filter']].concat(dropDownOptions) : dropDownOptions;
940     var store = new Ext.data.SimpleStore({'id': 0, fields: ['value', 'text'], data: options });
941     var combo = new Ext.form.ComboBox({
942         store:store,
943         forceSelection:true,
944         valueField:'value',
945         displayField:'text',
946         mode:'local',
947         allowBlank:false,
948         triggerAction:'all',
949         value:filterOp ? filterOp : ((!first) ? '' : defaultFilterTypes[type].getURLSuffix())
950     });
951     combo.on("select", function(combo, record, itemNo) {
952         var filter = this.getFilterType();
953         if (this.valueEditor)
954             this.valueEditor.setVisible(filter != null && filter.isDataValueRequired());
955     });
956 
957     combo.getFilterType = function () {
958         return LABKEY.Filter.getFilterTypeForURLSuffix(this.getValue());
959     };
960 
961     return combo;
962 };
963 
964 
965 // Check column plugin
966 Ext.grid.CheckColumn = function(config){
967     Ext.apply(this, config);
968     if(!this.id){
969         this.id = Ext.id();
970     }
971     this.renderer = this.renderer.createDelegate(this);
972 };
973 
974 Ext.grid.CheckColumn.prototype ={
975     init : function(grid){
976         this.grid = grid;
977         if(grid.getView() && grid.getView().mainBody)
978         {
979             grid.getView().mainBody.on('mousedown', this.onMouseDown, this);
980         }
981         else
982         {
983             this.grid.on('render', function(){
984                 var view = this.grid.getView();
985                 view.mainBody.on('mousedown', this.onMouseDown, this);
986             }, this);
987         }
988     },
989 
990     onMouseDown : function(e, t){
991         if(t.className && t.className.indexOf('x-grid3-cc-'+this.id) != -1){
992             e.stopEvent();
993             var index = this.grid.getView().findRowIndex(t);
994             var record = this.grid.store.getAt(index);
995             this.grid.getSelectionModel().selectRow(index);
996             record.set(this.dataIndex, !record.data[this.dataIndex]);
997         }
998     },
999 
1000     renderer : function(v, p, record){
1001         p.css += ' x-grid3-check-col-td';
1002         return '<div class="x-grid3-check-col'+(v?'-on':'')+' x-grid3-cc-'+this.id+'"> </div>';
1003     }
1004 };
1005