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  /**
 21   * @class <font color="red">DEPRECATED</font> - Consider using
 22   * <a href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a> instead.
 23   * <p> The LABKEY.ext.EditorGridPanel class is very similar to this class, except that it is a proper
 24   * extension of the Ext.grid.EditorGridPanel class, and thus exposes all of its properties, methods,
 25   * and events, and can participate in complex Ext layouts.</p>
 26   * <p> To transition from this class to the new LABKEY.ext.EditorGridPanel class, follow these steps:
 27   * <ul>
 28   * <li>Create a new LABKEY.ext.EditorGridPanel instead of a LABKEY.GridView</li>
 29   * <li>Ensure that you create the class after the page has fully loaded. Use the Ext.onReady() function to
 30   * specify a function to execute after the page has fully loaded. See the example in the
 31   * LABKEY.ext.EditorGridPanel class documentation.</li>
 32   * <li>In the new grid, the data store configuration has been separated from the grid configuration.
 33   * Therefore, you should move the schemaName, queryName, viewName, and containerPath config properties to
 34   * the config for the LABKEY.ext.Store you create for the value of the 'store' config property. See
 35   * the example in LABKEY.ext.EditorGridPanel class documentation.</li>
 36   * <li>If you specify a value for the renderTo config property, there is no need to call the
 37   * render() method as there was when using the old LABKEY.GridView.</li>
 38   * </ul>
 39   * @constructor
 40   * @param {Object} config Describes the GridView's properties.
 41   * @param {Object} config.schemaName Name of a schema defined within the current
 42   *                 container.  Example: 'study'.  See also: <a class="link"
 43                     href="https://www.labkey.org/Documentation/wiki-page.view?name=findNames">
 44                     How To Find schemaName, queryName & viewName</a>.
 45   * @param {Object} config.queryName Name of a query defined within the specified schema
 46   *                 in the current container.  Example: 'SpecimenDetail'. See also: <a class="link"
 47                     href="https://www.labkey.org/Documentation/wiki-page.view?name=findNames">
 48                     How To Find schemaName, queryName & viewName</a>.
 49   * @param {Object} [config.viewName] Name of a custom view defined over the specified query.
 50   *                 in the current container. Example: 'SpecimenDetail'.  See also: <a class="link"
 51                     href="https://www.labkey.org/Documentation/wiki-page.view?name=findNames">
 52                     How To Find schemaName, queryName & viewName</a>.
 53   * @param {String} config.renderTo Name of the div in which to place the grid.
 54   * @param {Bool} config.editable Whether the grid should be made editable.  Note that
 55   *                 not all tables and columns are editable, and not all users have
 56   *                 permission to edit.  For this reason, part or all of the grid may
 57   *                 degrade to being non-editable despite the 'editable' parameter.
 58   * @param {Object} [config.gridPanelConfig] Sets the display configuration for the new grid.  This
 59   *                 configuration is passed through to the underlying Ext.grid.GridPanel implementation,
 60   *                 so all <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.GridPanel">
 61   *                 GridPanel config options</a> are valid. <p/>Note that providing this configuration
 62   *                 is optional. Further, if you do provide it, you take responsibility for
 63   *                 providing a valid and complete config object.  If you do not set the
 64   *                 GridPanel config, LabKey Server will use a default configuration option.
 65   * @param {Object} [config.storeConfig] Config object that is passed to the underlying Store.
 66   *                 This configuration is passed through to the underlying Ext.data.Store implementation,
 67   *                 so all <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.data.Store">
 68   *                 Store config options</a> are valid. <p/>Note that providing this configuration
 69   *                 is optional. Further, if you do provide it, you take responsibility for
 70   *                 providing a valid and complete config object.  If you do not set the
 71   *                 Store config, LabKey Server will use a default configuration option.
 72   * @param {Function(columnModel)} [config.columnModelListener] Callback function that allows
 73   *					you to adjust the column
 74   *					model without providing a full GridPanel config.  The columnModel
 75   *					element/object contains information about how one may interact with
 76   *					the columns within a user interface. This format is generated to match
 77   *					the requirements of the Ext grid component.  See
 78   *					<a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.ColumnModel">
 79   *					Ext.grid.ColumnModel</a> for further information.
 80   * @param {Function(Ext.grid.GridPanel)} config.gridCustomizeCallback Function that should be called after the
 81   *					grid has been constructed and populated with data. You can use this to
 82   *					further customize the grid's appearance, add toolbar buttons, or call
 83   *					any method on the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.GridPanel">
 84   *                 Ext GridPanel object</a>.  The function passed as this config property
 85   *					should look like this:
 86   * @param {String} [config.containerPath] The container path in which the schemaName and queryName are defined.
 87   *                 If not supplied, the current container path will be used.
 88 */
 89 
 90 LABKEY.GridView = function(config)
 91 {
 92     Ext.QuickTips.init();
 93     
 94     Date.patterns = {
 95         ISO8601Long:"Y-m-d H:i:s",
 96         ISO8601Short:"Y-m-d"
 97     };
 98 
 99     if (!config.schemaName || !config.queryName)
100     {
101         Ext.Msg.alert("Configuration Error", "config.schemaName and config.queryName are required parameters");
102         return;
103     }
104 
105     var _primarySchemaName = config.schemaName;
106     var _primaryQueryName = config.queryName;
107     var _primaryViewName = config.viewName;
108     var _renderTo = config.renderTo;
109     var _gridPanelConfig = config.gridPanelConfig;
110     var _storeConfig = config.storeConfig;
111     var _selectionModel;
112     var _editable = config.editable;
113     var _errorsInGridData = false;
114 
115     // private member variables:
116     var _ds;
117     var _myReader;
118     var _columnModelListener = config.columnModelListener;
119 	var _gridCustomizeCallback = config.gridCustomizeCallback;
120     var _pageLimit = 20;
121     var _grid;
122     var _containerPath = config.containerPath;
123 
124 
125     // private methods:
126     function getDefaultRenderer(fieldColumn, displayColumn)
127     {
128         switch (fieldColumn.type)
129         {
130             case "date":
131                 return function(data)
132                 {
133                     if (!data)
134                         return;
135                     var date = new Date(data);
136                     if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0)
137                         return date.format(Date.patterns.ISO8601Short);
138                     else
139                         return date.format(Date.patterns.ISO8601Long)
140                 };
141                 break;
142             case "boolean":
143             case "int":
144             case "float":
145             case "string":
146             default:
147         }
148     }
149 
150     function getDefaultEditor(fieldColumn, displayColumn)
151     {
152         if (displayColumn.editable)
153         {
154             var editor;
155             switch (fieldColumn.type)
156             {
157                 case "boolean":
158                     editor = new Ext.form.Checkbox();
159                     break;
160                 case "int":
161                     editor = new Ext.form.NumberField({
162                         allowDecimals : false
163                     });
164                     break;
165                 case "float":
166                     editor = new Ext.form.NumberField({
167                         allowDecimals : true
168                     });
169                     break;
170                 case "date":
171                     editor = new Ext.form.DateField({
172                         format : Date.patterns.ISO8601Long,
173                         altFormats: Date.patterns.ISO8601Short +
174                                     '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|' +
175                                     '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|' +
176                                     'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
177                                     'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
178                                     'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
179                                     'n/j/y|n/j/Y|' +
180                                     'n-j-y|n-j-Y|' +
181                                     'j-M-y|j-M-Y|' +
182                                     'Y-n-d H:i:s|Y-n-d'
183                     });
184                     break;
185                 case "string":
186                     editor = new Ext.form.TextField();
187                     break;
188                 default:
189             }
190             if (editor)
191             {
192                 editor.allowBlank = !displayColumn.required;
193                 registerEditorListeners(editor);
194             }
195             return editor;
196         }
197     }
198 
199     function getLookupEditor(dsLookup, lookupDef, allowNull)
200     {
201         var editor = new Ext.form.ComboBox({ //dropdown based on server side data (from db)
202                         typeAhead: false, //will be querying database so may not want typeahead consuming resources
203                         triggerAction: 'all',
204                         editable:false,
205                         lazyRender: true,//prevents combo box from rendering until requested, should always be true for editor
206                         store: dsLookup,//Industry,//where to get the data for our combobox
207                         displayField: lookupDef.displayColumn,//the underlying data  field name to bind to this ComboBox
208                                          //(defaults to undefined if mode = 'remote' or 'text' if transforming a select)
209                         valueField: lookupDef.keyColumn,     //the underlying value field name to bind to this ComboBox
210                         tpl : '<tpl for="."><div class="x-combo-list-item">{[values["' + lookupDef.displayColumn + '"]]}</div></tpl>',
211                         allowBlank : allowNull
212                     });
213         registerEditorListeners(editor);
214         return editor;
215     }
216 
217     function registerEditorListeners(editor)
218     {
219         editor.addListener("complete", afterCellEdit);
220         editor.addListener("beforeshow", function()
221         {
222             document.ActiveExtGridViewCellId = editor.id;
223             return true;
224         });
225     }
226 
227     // this function creates a closure that allows the references to dsLookup and
228     // lookupDef to stick to the render function:
229     function getLookupRenderer(dsLookup, lookupDef)
230     {
231         var refreshed = false;
232         dsLookup.on("load", function(store, recordArray, options)
233         {
234             if (_grid && !refreshed)
235                 _grid.getView().refresh();
236             refreshed = true;
237             store.un("load", this);
238         });
239 
240         return function(data)
241         {
242             var record = dsLookup.getById(data);
243             if (record)
244                 return record.data[lookupDef.displayColumn];
245             else if (data)
246                 return '[' + data + ']';
247             else
248                 return '[None]';
249         };
250     }
251 
252     function initColumnUI()
253     {
254         var columnModelNameMap = {};
255         for (var columnId in _myReader.jsonData.columnModel)
256         {
257             var column = _myReader.jsonData.columnModel[columnId];
258             columnModelNameMap[column.dataIndex] = column;
259         }
260 
261         for (var fieldId = 0; fieldId < _myReader.jsonData.metaData.fields.length; fieldId++)
262         {
263             var fieldColumn = _myReader.jsonData.metaData.fields[fieldId];
264             var displayColumn = columnModelNameMap[fieldColumn.name];
265             if (fieldColumn.lookup)
266             {
267                 var lookupDef = fieldColumn.lookup;
268                 var allowNull = !displayColumn.required;
269                 var storeConfig = {schemaName: lookupDef.schema, queryName: lookupDef.table, containerPath: _containerPath};
270                 if (allowNull)
271                 {
272                     storeConfig.allowNull = { keyColumn: lookupDef.keyColumn, displayColumn: lookupDef.displayColumn };
273                 }
274                 var dsLookup = LABKEY.ext.Utils.createExtStore(storeConfig);
275 
276                 displayColumn.renderer = getLookupRenderer(dsLookup, lookupDef);
277                 if (_editable)
278                     displayColumn.editor = getLookupEditor(dsLookup, lookupDef, allowNull);
279 
280                 dsLookup.load();
281             }
282             else
283             {
284                 if (_editable)
285                     displayColumn.editor = getDefaultEditor(fieldColumn, displayColumn);
286                 displayColumn.renderer = getDefaultRenderer(fieldColumn, displayColumn);
287             }
288         }
289     }
290 
291     function handleGridData(r, options, success)
292     {
293         if (!success)
294             return;
295 
296         if (!_myReader || !_myReader.jsonData || !_myReader.jsonData.columnModel)
297             return;
298 
299         if (!_gridPanelConfig)
300             _gridPanelConfig = {};
301 
302         _gridPanelConfig.store = _ds;
303 
304         if (_columnModelListener)
305             _gridPanelConfig.columns = _columnModelListener(_myReader.jsonData.columnModel)
306         else
307             _gridPanelConfig.columns = _myReader.jsonData.columnModel;
308 
309         // double check to see if there are any editable columns in this col model.  If not,
310         // degrade to a non-editable grid.
311         if (_editable)
312         {
313             var anyEditable = false;
314             for (var i = 0; i < _gridPanelConfig.columns.length && !anyEditable; i++)
315                 anyEditable = _gridPanelConfig.columns[i].editable;
316             if (!anyEditable)
317                 _editable = false;
318         }
319 
320         initColumnUI();
321 
322         if (!_gridPanelConfig.view)
323         {
324             _gridPanelConfig.view = new Ext.grid.GridView({
325                     forceFit:true,
326                     // custom grouping text template to display the number of items per group
327                     groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'
328                 });
329         }
330 
331         if (!_gridPanelConfig.selModel)
332         {
333             if (_editable)
334             {
335                 _gridPanelConfig.selModel = new Ext.grid.CheckboxSelectionModel({singleSelect:false});
336                 _gridPanelConfig.columns = [_gridPanelConfig.selModel].concat(_gridPanelConfig.columns);
337             }
338             else
339                 _gridPanelConfig.selModel = new Ext.grid.RowSelectionModel({singleSelect:true});
340         }
341         _selectionModel = _gridPanelConfig.selModel;
342 
343         if (_editable)
344         {
345             if (!_gridPanelConfig.clicksToEdit)
346                 _gridPanelConfig.clicksToEdit = 2;
347 
348             if (!_gridPanelConfig.tbar)
349             {
350                 _gridPanelConfig.tbar = [
351                     {
352                         text: 'Add Record',
353                         tooltip: 'Click to add a row',
354                         handler: addRecord, //what happens when user clicks on it
355                         id: 'add-record-button'
356                     }, '-', //add a separator
357                     {
358                         text: 'Delete Selected',
359                         tooltip: 'Click to delete selected row(s)',
360                         handler: handleDelete, //what happens when user clicks on it
361                         id: 'delete-records-button'
362                     }, '-', //add a separator
363                     {
364                         text: 'Refresh',
365                         tooltip: 'Click to refresh the table',
366                         id: 'refresh-button',
367                         handler: refreshGrid //what happens when user clicks on it
368                     }
369                 ];
370             }
371             _grid = new Ext.grid.EditorGridPanel(_gridPanelConfig);
372             _ds.addListener("beforeload", commitEdits);
373             _grid.addListener('afteredit', afterCellEdit);//give event name, handler (can use 'on' shorthand for addListener)
374             _grid.addListener('beforeedit', beforeCellEdit);//give event name, handler (can use 'on' shorthand for addListener)
375             _editCommitTimer = new Ext.util.DelayedTask(commitEdits, this)
376         }
377         else
378         {
379             if (!_gridPanelConfig.tbar)
380             {
381                 _gridPanelConfig.tbar = [
382                     {
383                         text: 'Refresh',
384                         tooltip: 'Click to Refresh the table',
385                         handler: refreshGrid //what happens when user clicks on it
386                     }
387                 ];
388             }
389             _grid = new Ext.grid.GridPanel(_gridPanelConfig);
390         }
391 
392         //call the grid customize callback if any
393         if(_gridCustomizeCallback)
394             _gridCustomizeCallback(_grid);
395     }
396 
397     function typeConvert(fieldColumn, value)
398     {
399         if (!value && !fieldColumn.required)
400             return null;
401         switch (fieldColumn.type)
402         {
403             case "boolean":
404                 // strange trick here to boolean-ify our value.  From
405                 // http://www.jibbering.com/faq/faq_notes/type_convert.html
406                 return value instanceof Boolean ? value : !!value;
407             case "int":
408                 return value instanceof Number ? value : Math.round(Number(value));
409             case "float":
410                 return value instanceof Number ? value : Number(value);
411             case "date":
412                 return value instanceof Date ? value : Date(value);
413             case "string":
414                 return value instanceof String ? value : "" + value;
415             default:
416                 return value;
417         }
418     }
419 
420     function getDefaultValues(fields)
421     {
422         var record = {};
423         for (var i = 0; i < fields.length; i++)
424         {
425             var field = fields[i];
426             record[field.name] = null;
427         }
428         return record;
429     }
430 
431     function addRecord()
432     {
433         var fields = _myReader.jsonData.metaData.fields;
434         var recordCreator = Ext.data.Record.create(fields);
435         var newRecord = new recordCreator(getDefaultValues(fields));
436         newRecord.LABKEY$isNew = true;
437         _grid.stopEditing();//stops any acitve editing
438         _ds.insert(0, newRecord); //1st arg is index,
439                          //2nd arg is Ext.data.Record[] records
440         //very similar to ds.add, with ds.insert we can specify the insertion point
441         _grid.startEditing(0, 1);//starts editing the specified rowIndex, colIndex
442                                 //make sure you pick an editable location in the line above
443                                 //otherwise it won't initiate the editor
444     }
445 
446     function isNullRecord(record)
447     {
448         for (var field in record)
449         {
450             var value = record[field];
451             if (value)
452                 return false;
453         }
454         return true;
455     }
456 
457     var _currentEditRow;
458     var _editCommitTimer;
459     function beforeCellEdit(parameters)
460     {
461         if (_currentEditRow == parameters.row)
462             _editCommitTimer.cancel();
463         _currentEditRow = parameters.row;
464         _selectionModel.selectRow(parameters.row);
465         var record = _ds.getAt(parameters.row);
466         record.saveNeeded = true;
467     }
468 
469     function afterCellEdit(parameters)
470     {
471         _editCommitTimer.delay(250);
472         _errorsInGridData = false;
473     }
474 
475     function commitEdits()
476     {
477         var keyColumn = _myReader.jsonData.metaData.id;
478         var records = _ds.getModifiedRecords();
479         for (var i = 0; i < records.length; i++)
480         {
481             var record = records[i];
482             if (!record.data.toBeDeleted && (record.data[keyColumn] || !isNullRecord(record.data)))
483                 updateDB(record)
484         }
485     }
486 
487     function handleDelete()
488     {
489         if (_selectionModel)
490         {
491             commitEdits();
492             var records = _selectionModel.getSelections();
493             if (records && records.length)
494             {
495                 if (confirm("Permanently delete the selected records?"))
496                 {
497                     var data = [];
498                     var keyColumn = _myReader.jsonData.metaData.id;
499                     var uncommittedRecords = false;
500                     for (var i = 0; i < records.length; i++)
501                     {
502                         var recordData = records[i].data;
503                         if (recordData[keyColumn])
504                             data[data.length] = recordData;
505                         else
506                         {
507                             uncommittedRecords = true;
508                             recordData.toBeDeleted = true;
509                         }
510                     }
511                     if (data.length > 0)
512                     {
513                         LABKEY.Query.deleteRows(_primarySchemaName, _primaryQueryName, data,
514                                 afterSuccessfulDelete, afterFailedEdit);
515                     }
516                     else if (uncommittedRecords)
517                         refreshGrid();
518                 }
519             }
520         }
521     }
522 
523     function refreshGrid()
524     {
525         _ds.reload();
526     }
527 
528     function afterSuccessfulDelete(responseObj, options)
529     {
530         refreshGrid();
531     }
532 
533     function afterSuccessfulEdit(responseObj, options)
534     {
535         commitSavedRows(responseObj);
536         //_ds.commitChanges();//commit changes (removes the red triangle which indicates a 'dirty' field)
537        // refreshGrid();
538     }
539 
540     function commitSavedRows(responseObj)
541     {
542         if (!responseObj.rows || responseObj.rows.length == 0)
543             return;
544 
545         for (var rowIndex = 0; rowIndex < responseObj.rows.length; rowIndex++)
546         {
547             var keyValue = responseObj.rows[rowIndex][_myReader.jsonData.metaData.id];
548             var record = _ds.getById(keyValue);
549 
550             if(record)
551             {
552                 //set all fields that are present in the rows[rowIndex] object
553                 var retRow = responseObj.rows[rowIndex];
554                 for(var field in record.data)
555                 {
556                     if(retRow[field])
557                         record.set(field, retRow[field]);
558                 }
559 
560                 record.commit();
561             }
562         }
563     }
564 
565     function getAfterSuccessfulEdit(record)
566     {
567         return function(responseObj, options)
568         {
569             record.operationPendingSinceLastEdit = false;
570             record.commit();
571 
572             //the key value may have changed in response to the edit
573             //(study dataset case)
574             var retRecord = responseObj.rows[0];
575             var idCol = _myReader.jsonData.metaData.id;
576             if(retRecord[idCol] != record[idCol])
577             {
578                 //if the key changed, we need to create a new record,
579                 //add it to the store, and remove the old one. Ext
580                 //has no way to update the key of an existing record.
581                 var recordCreator = Ext.data.Record.create(_myReader.jsonData.metaData.fields);
582                 var newKeyRecord = new recordCreator(retRecord, retRecord[idCol]);
583                 _ds.insert(_ds.indexOf(record), newKeyRecord);
584                 _ds.remove(record);
585             }
586             else
587             {
588                 //even if the key didn't change, other fields may
589                 //have been modified at the server, so copy over the
590                 //values that were returned
591                 for(var field in retRecord.data)
592                     record.set(field, retRecord[field]);
593             }
594 
595         }
596     }
597 
598     function getAfterSuccessfulInsert(newRecord)
599     {
600         return function(responseObj, options)
601         {
602             newRecord.operationPendingSinceLastEdit = false;
603             newRecord.commit();
604 
605             //create a new record based on the fields returned from the server
606             //and specify the newly-assigned id
607             //and remove the temporary newRecord
608             var row = responseObj.rows[0];
609             var fields = _myReader.jsonData.metaData.fields;
610             var recordCreator = Ext.data.Record.create(fields);
611             var newNewRecord = new recordCreator(row, row[_myReader.jsonData.metaData.id]);
612             _ds.insert(_ds.indexOf(newRecord), newNewRecord);
613             _ds.remove(newRecord);
614         }
615     }
616 
617     function afterFailedEdit(jsonResponse, options)
618     {
619         _errorsInGridData = true;
620         if (jsonResponse && jsonResponse.exception)
621         {
622             Ext.Msg.alert("Update Failed", jsonResponse.exception + "\n(Exception class " + jsonResponse.exceptionClass + ")")
623         }
624         else
625             Ext.Msg.alert("Update Failed", jsonResponse.statusText + " (Response code " + jsonResponse.status + ")");
626     }
627 
628     function updateDB(record)
629     {
630         if (_errorsInGridData)
631             return;
632 
633         if (!record.saveNeeded)
634             return;
635         /*
636         * editEvent has the following properties:
637         * grid - This grid
638         * record - The record being edited
639         * field - The field name being edited
640         * value - The value being set
641         * originalValue - The original value for the field, before the edit.
642         * row - The grid row index
643         * column - The grid column index
644         */
645         var store = _grid.getStore();
646         var fields = store.fields;
647         var recordData = {};
648 
649         for (var fieldId = 0; fieldId < fields.length; fieldId++)
650         {
651             var field = fields.itemAt(fieldId);
652             var value = record.data[field.name];
653             if (value != null)
654                 value = typeConvert(field, value);
655             recordData[field.name] = value;
656         }
657 
658         var validRecord = true;
659         for (var colId = 0; colId < _myReader.jsonData.columnModel.length && validRecord; colId++)
660         {
661             var col = _myReader.jsonData.columnModel[colId];
662             // we allow a null key column, since that's always going to be the case for insert
663             if (col.dataIndex != store.reader.jsonData.metaData.id)
664             {
665                 if (recordData[col.dataIndex] == null && col.required)
666                     validRecord = false;
667             }
668         }
669 
670 
671         if (validRecord)
672         {
673             record.saveNeeded = false;
674             record.operationPendingSinceLastEdit = true;
675             if (record.LABKEY$isNew) //!recordData[store.reader.jsonData.metaData.id])
676             {
677                 LABKEY.Query.insertRows(_primarySchemaName, _primaryQueryName, [recordData],
678                         getAfterSuccessfulInsert(record), afterFailedEdit);
679             }
680             else
681             {
682                 LABKEY.Query.updateRows(_primarySchemaName, _primaryQueryName, [recordData],
683                         getAfterSuccessfulEdit(record), afterFailedEdit);
684             }
685         }
686     }
687 
688 
689 
690     function createDefaultStoreImpl()
691     {
692         if (!_storeConfig)
693             _storeConfig = {};
694         _storeConfig.schemaName = _primarySchemaName;
695         _storeConfig.queryName = _primaryQueryName;
696         _storeConfig.viewName = _primaryViewName;
697         _storeConfig.containerPath = _containerPath;
698         return LABKEY.ext.Utils.createExtStore(_storeConfig);
699     }
700 
701     function displayGridImpl()
702     {
703         if (!_gridPanelConfig)
704             _gridPanelConfig = {};
705         _gridPanelConfig.renderTo = _renderTo;
706 
707         _ds = createDefaultStoreImpl();
708         _myReader = _ds.reader;
709 
710         if (!_gridPanelConfig.bbar)
711         {
712             _gridPanelConfig.bbar = new Ext.PagingToolbar({
713                     pageSize: _pageLimit,//default is 20
714                     store: _ds,
715                   //  paramNames : _extParamMapping,
716                     emptyMsg: "No data to display"//display message when no records found
717                 });
718         }
719 
720         _ds.load({ callback : handleGridData,
721             params : {
722                 start: 0,
723                 limit: _pageLimit
724             }});
725     }
726 
727     // public methods:
728     /** @scope LABKEY.GridView.prototype */
729     return {
730 	  /**
731 	  *   Renders the grid view to the div specified in the renderTo config property.
732 	  */
733         render : function()
734         {
735             return displayGridImpl();
736         },
737 
738         /**
739          * Returns the Ext.data.Store used to manage the data displayed in the grid.
740          * You can use the returned object to programmatically manipulate the store.
741          * <p/>
742          * See <a href='http://www.extjs.com/deploy/dev/docs/?class=Ext.data.Store'>
743          * http://www.extjs.com/deploy/dev/docs/?class=Ext.data.Store</a> for more
744          * information on the Ext.data.Store class.
745          * 
746          * @example Example:
747          * <pre name="code" class="xml">
748          * //this code will programmatically refresh the data
749          * //displayed in the myGrid object
750          * myGrid.getStore().reload();
751          * </pre>
752          */
753         getStore : function()
754         {
755             return _grid.getStore();
756         }
757     }
758 };
759 
760