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 Store using the supplied configuration.
 24  * @class LabKey extension to the <a href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.data.Store">Ext.data.Store</a> class,
 25  * which can retrieve data from a LabKey server, track changes, and update the server upon demand. This is most typically
 26  * used with data-bound user interface widgets, such as the <a href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a>.
 27  *
 28  * <p>If you use any of the LabKey APIs that extend Ext APIs, you must either make your code open source or
 29  * <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=extDevelopment">purchase an Ext license</a>.</p>
 30  *            <p>Additional Documentation:
 31  *              <ul>
 32  *                  <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=javascriptTutorial">Tutorial: Create Applications with the JavaScript API</a></li>
 33  *              </ul>
 34  *           </p>
 35  * @constructor
 36  * @augments Ext.data.Store
 37  * @param config Configuration properties.
 38  * @param {String} config.schemaName The LabKey schema to query.
 39  * @param {String} config.queryName The query name within the schema to fetch.
 40  * @param {String} [config.sql] A LabKey SQL statement to execute to fetch the data. You may specify either a queryName or sql,
 41  * but not both. Note that when using sql, the store becomes read-only, as it has no way to know how to update/insert/delete the rows.
 42  * @param {String} [config.viewName] A saved custom view of the specified query to use if desired.
 43  * @param {String} [config.columns] A comma-delimited list of column names to fetch from the specified query. Note
 44  *  that the names may refer to columns in related tables using the form 'column/column/column' (e.g., 'RelatedPeptide/TrimmedPeptide').
 45  * @param {String} [config.sort] A base sort specification in the form of '[-]column,[-]column' ('-' is used for descending sort).
 46  * @param {Array} [config.filterArray] An array of LABKEY.Filter.FilterDefinition objects to use as the base filters.
 47  * @param {Boolean} [config.updatable] Defaults to true. Set to false to prohibit updates to this store.
 48  * @param {String} [config.containerPath] The container path from which to get the data. If not specified, the current container is used.
 49  * @param {Integer} [config.maxRows] The maximum number of rows returned by this query (defaults to showing all rows).
 50  * @param {Boolean} [config.ignoreFilter] True will ignore any filters applied as part of the view (defaults to false).
 51  * @param {String} [config.containerFilter] The container filter to use for this query (defaults to null).
 52  *      Supported values include:
 53  *       <ul>
 54  *           <li>"Current": Include the current folder only</li>
 55  *           <li>"CurrentAndSubfolders": Include the current folder and all subfolders</li>
 56  *           <li>"CurrentPlusProject": Include the current folder and the project that contains it</li>
 57  *           <li>"CurrentAndParents": Include the current folder and its parent folders</li>
 58  *           <li>"CurrentPlusProjectAndShared": Include the current folder plus its project plus any shared folders</li>
 59  *           <li>"AllFolders": Include all folders for which the user has read permission</li>
 60  *       </ul>
 61  * @example <div id="div1"/>
 62  <script type="text/javascript">
 63 
 64  // This sample code uses LABKEY.ext.Store to hold data from the server's Users table.
 65  // Ext.grid.EditorGridPanel provides a user interface for updating the Phone column.
 66  // On pressing the 'Submit' button, any changes made in the grid are submitted to the server.
 67  var _store = new LABKEY.ext.Store({
 68     schemaName: 'core',
 69     queryName: 'Users',
 70     columns: "DisplayName, Phone",
 71     autoLoad: true
 72 });
 73 
 74  var _grid = new Ext.grid.EditorGridPanel({
 75     title: 'Users - Change Phone Number',
 76     store: _store,
 77     renderTo: 'div1',
 78     autoHeight: true,
 79     columnLines: true,
 80     viewConfig: {
 81         forceFit: true
 82     },
 83     colModel: new Ext.grid.ColumnModel({
 84         columns: [{
 85             header: 'User Name',
 86             dataIndex: 'DisplayName',
 87             hidden: false,
 88             width: 150
 89         }, {
 90             header: 'Phone Number',
 91             dataIndex: 'Phone',
 92             hidden: false,
 93             sortable: true,
 94             width: 100,
 95             editor: new Ext.form.TextField()
 96         }]
 97     }),
 98     buttons: [{
 99         text: 'Save',
100         handler: function ()
101         {
102             _grid.getStore().commitChanges();
103             alert("Number of records changed: "
104                    + _grid.getStore().getModifiedRecords().length);
105         }
106     }, {
107         text: 'Cancel',
108         handler: function ()
109         {
110             _grid.getStore().rejectChanges();
111         }
112     }]
113 });
114 
115  </script>
116  */
117 LABKEY.ext.Store = Ext.extend(Ext.data.Store, {
118     constructor: function(config) {
119 
120         var baseParams = {schemaName: config.schemaName};
121         var qsParams = {};
122 
123         if (config.queryName && !config.sql)
124             baseParams['query.queryName'] = config.queryName;
125         if (config.sql)
126         {
127             baseParams.sql = config.sql;
128             config.updatable = false;
129         }
130         if (config.sort)
131         {
132             baseParams['query.sort'] = config.sort;
133 
134             if (config.sql)
135                 qsParams['query.sort'] = config.sort;
136         }
137         else if (config.sortInfo) {
138             var sInfo = ("DESC" == config.sortInfo.direction ? '-' : '') + config.sortInfo.field;
139             baseParams['query.sort'] = sInfo;
140 
141             if (config.sql)
142                 qsParams['query.sort'] = sInfo;
143         }
144 
145         if (config.parameters)
146         {
147             for (var n in config.parameters)
148                 baseParams["query.param." + n] = config.parameters[n];
149         }
150 
151         //important...otherwise the base Ext.data.Store interprets it
152         delete config.sort;
153         delete config.sortInfo;
154 
155         if (config.viewName && !config.sql)
156             baseParams['query.viewName'] = config.viewName;
157 
158         if (config.columns && !config.sql)
159             baseParams['query.columns'] = Ext.isArray(config.columns) ? config.columns.join(",") : config.columns;
160 
161         if (config.containerFilter)
162             baseParams.containerFilter = config.containerFilter;
163 
164         if(config.ignoreFilter)
165             baseParams['query.ignoreFilter'] = 1;
166 
167         if(config.maxRows)
168         {
169             // Issue 16076, executeSql needs maxRows not query.maxRows.
170             if (config.sql)
171                 baseParams['maxRows'] = config.maxRows;
172             else
173                 baseParams['query.maxRows'] = config.maxRows;
174         }
175 
176         baseParams.apiVersion = 9.1;
177 
178         Ext.apply(this, config, {
179             remoteSort: true,
180             updatable: true
181         });
182 
183         // Set of parameters that are reserved for query
184         this.queryParamSet = {
185             columns : true,
186             containerFilterName : true,
187             ignoreFilter : true,
188             maxRows : true,
189             param   : true,
190             queryName : true,
191             sort    : true,
192             viewName: true
193         };
194 
195         this.isLoading = false;
196 
197         LABKEY.ext.Store.superclass.constructor.call(this, {
198             reader: new LABKEY.ext.ExtendedJsonReader(),
199             proxy: this.proxy ||
200                     new Ext.data.HttpProxy(new Ext.data.Connection({
201                         method: 'POST',
202                         url: (config.sql ? LABKEY.ActionURL.buildURL("query", "executeSql", config.containerPath, qsParams)
203                                 : LABKEY.ActionURL.buildURL("query", "selectRows", config.containerPath)),
204                         listeners: {
205                             beforerequest: {fn: this.onBeforeRequest, scope: this}
206                         },
207                         timeout: Ext.Ajax.timeout
208                     })),
209             baseParams: baseParams,
210             autoLoad: false
211         });
212 
213         this.on('beforeload', this.onBeforeLoad, this);
214         this.on('load', this.onLoad, this);
215         this.on('loadexception', this.onLoadException, this);
216         this.on('update', this.onUpdate, this);
217 
218         //Add this here instead of using Ext.store to make sure above listeners are added before 1st load
219         if(config.autoLoad){
220             this.load.defer(10, this, [ Ext.isObject(this.autoLoad) ? this.autoLoad : undefined ]);
221         }
222 
223         /**
224          * @memberOf LABKEY.ext.Store#
225          * @name beforecommit
226          * @event
227          * @description Fired just before the store sends updated records to the server for saving. Return
228          * false from this event to stop the save operation.
229          * @param {array} records An array of Ext.data.Record objects that will be saved.
230          * @param {array} rows An array of simple row-data objects from those records. These are the actual
231          * data objects that will be sent to the server.
232          */
233         /**
234          * @memberOf LABKEY.ext.Store#
235          * @name commitcomplete
236          * @event
237          * @description Fired after all modified records have been saved on the server.
238          */
239         /**
240          * @memberOf LABKEY.ext.Store#
241          * @name commitexception
242          * @event
243          * @description Fired if there was an exception during the save process.
244          * @param {String} message The exception message.
245          */
246         this.addEvents("beforecommit", "commitcomplete", "commitexception");
247     },
248 
249     /**
250      * Adds a new record to the store based upon a raw data object.
251      * @name addRecord
252      * @function
253      * @memberOf LABKEY.ext.Store#
254      * @param {Object} data The raw data object containing a properties for each field.
255      * @param {integer} [index] The index at which to insert the record. If not supplied, the new
256      * record will be added to the end of the store.
257      * @returns {Ext.data.Record} The new Ext.data.Record object.
258      */
259     addRecord : function(data, index) {
260         if (!this.updatable)
261             throw "this LABKEY.ext.Store is not updatable!";
262 
263         if(undefined == index)
264             index = this.getCount();
265 
266         var fields = this.reader.meta.fields;
267 
268         //if no data was passed, create a new object with
269         //all nulls for the field values
270         if(!data)
271             data = {};
272 
273         //set any non-specified field to null
274         //some bound control (like the grid) need a property
275         //defined for each field
276         var field;
277         for(var idx = 0; idx < fields.length; ++idx)
278         {
279             field = fields[idx];
280             if(!data[field.name])
281                 data[field.name] = null;
282         }
283 
284         var recordConstructor = Ext.data.Record.create(fields);
285         var record = new recordConstructor(data);
286 
287         //add an isNew property so we know that this is a new record
288         record.isNew = true;
289         this.insert(index, record);
290         return record;
291     },
292 
293     /**
294      * Deletes a set of records from the store as well as the server. This cannot be undone.
295      * @name deleteRecords
296      * @function
297      * @memberOf LABKEY.ext.Store#
298      * @param {Array of Ext.data.Record objects} records The records to delete.
299      */
300     deleteRecords : function(records) {
301         if (!this.updatable)
302             throw "this LABKEY.ext.Store is not updatable!";
303 
304         if(!records || records.length == 0)
305             return;
306 
307         var deleteRowsKeys = [];
308         var key;
309         for(var idx = 0; idx < records.length; ++idx)
310         {
311             key = {};
312             key[this.idName] = records[idx].id;
313             deleteRowsKeys[idx] = key;
314         }
315 
316         //send the delete
317         LABKEY.Query.deleteRows({
318             schemaName: this.schemaName,
319             queryName: this.queryName,
320             containerPath: this.containerPath,
321             rows: deleteRowsKeys,
322             successCallback: this.getDeleteSuccessHandler(),
323             action: "deleteRows" //hack for Query.js bug
324         });
325     },
326 
327 
328     getChanges : function(records) {
329         records = records || this.getModifiedRecords();
330 
331         if(!records || records.length == 0)
332             return [];
333 
334         if (!this.updatable)
335             throw "this LABKEY.ext.Store is not updatable!";
336 
337         //build the json to send to the server
338         var record;
339         var insertCommand =
340         {
341             schemaName: this.schemaName,
342             queryName: this.queryName,
343             command: "insertWithKeys",
344             rows: []
345         };
346         var updateCommand =
347         {
348             schemaName: this.schemaName,
349             queryName: this.queryName,
350             command: "updateChangingKeys",
351             rows: []
352         };
353         for(var idx = 0; idx < records.length; ++idx)
354         {
355             record = records[idx];
356 
357             //if we are already in the process of saving this record, just continue
358             if(record.saveOperationInProgress)
359                 continue;
360 
361             //NOTE: this check could possibly be eliminated since the form/server should do the same thing
362             if(!this.readyForSave(record))
363                 continue;
364 
365             record.saveOperationInProgress = true;
366             //NOTE: modified since ext uses the term phantom for any record not saved to server
367             if (record.isNew || record.phantom)
368             {
369                 insertCommand.rows.push({
370                     values: this.getRowData(record),
371                     oldKeys : this.getOldKeys(record)
372                 });
373             }
374             else
375             {
376                 updateCommand.rows.push({
377                     values: this.getRowData(record),
378                     oldKeys : this.getOldKeys(record)
379                 });
380             }
381         }
382 
383         var commands = [];
384         if (insertCommand.rows.length > 0)
385         {
386             commands.push(insertCommand);
387         }
388         if (updateCommand.rows.length > 0)
389         {
390             commands.push(updateCommand);
391         }
392 
393         for(var i=0;i<commands.length;i++){
394             if(commands[i].rows.length > 0 && false === this.fireEvent("beforecommit", records, commands[i].rows))
395                 return [];
396         }
397 
398         return commands
399     },
400 
401     /**
402      * Commits all changes made locally to the server. This method executes the updates asynchronously,
403      * so it will return before the changes are fully made on the server. Records that are being saved
404      * will have a property called 'saveOperationInProgress' set to true, and you can test if a Record
405      * is currently being saved using the isUpdateInProgress method. Once the record has been updated
406      * on the server, its properties may change to reflect server-modified values such as Modified and ModifiedBy.
407      * <p>
408      * Before records are sent to the server, the "beforecommit" event will fire. Return false from your event
409      * handler to prohibit the commit. The beforecommit event handler will be passed the following parameters:
410      * <ul>
411      * <li><b>records</b>: An array of Ext.data.Record objects that will be sent to the server.</li>
412      * <li><b>rows</b>: An array of row data objects from those records.</li>
413      * </ul>
414      * <p>
415      * The "commitcomplete" or "commitexception" event will be fired when the server responds. The former
416      * is fired if all records are successfully saved, and the latter if an exception occurred. All modifications
417      * to the server are transacted together, so all records will be saved or none will be saved. The "commitcomplete"
418      * event is passed no parameters. The "commitexception" even is passed the error message as the only parameter.
419      * You may return false form the "commitexception" event to supress the default display of the error message.
420      * <p>
421      * For information on the Ext event model, see the
422      * <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.util.Observable">Ext API documentation</a>.
423      * @name commitChanges
424      * @function
425      * @memberOf LABKEY.ext.Store#
426      */    
427     commitChanges : function(){
428         var records = this.getModifiedRecords();
429         var commands = this.getChanges(records);
430 
431         if (!commands.length){
432             return false;
433         }
434 
435         Ext.Ajax.request({
436             url : LABKEY.ActionURL.buildURL("query", "saveRows", this.containerPath),
437             method : 'POST',
438             success: this.onCommitSuccess,
439             failure: this.getOnCommitFailure(records),
440             scope: this,
441             jsonData : {
442                 containerPath: this.containerPath,
443                 commands: commands
444             },
445             headers : {
446                 'Content-Type' : 'application/json'
447             }
448         });
449     },
450 
451     /**
452      * Returns true if the given record is currently being updated on the server, false if not.
453      * @param {Ext.data.Record} record The record.
454      * @returns {boolean} true if the record is currently being updated, false if not.
455      * @name isUpdateInProgress
456      * @function
457      * @memberOf LABKEY.ext.Store#
458      */
459     isUpdateInProgress : function(record) {
460         return record.saveOperationInProgress;
461     },
462 
463     /**
464      * Returns a LabKey.ext.Store filled with the lookup values for a given
465      * column name if that column exists, and if it is a lookup column (i.e.,
466      * lookup meta-data was supplied by the server).
467      * @param columnName The column name
468      * @param includeNullRecord Pass true to include a null record at the top
469      * so that the user can set the column value back to null. Set the
470      * lookupNullCaption property in this Store's config to override the default
471      * caption of "[none]".
472      */
473     getLookupStore : function(columnName, includeNullRecord)
474     {
475         if(!this.lookupStores)
476             this.lookupStores = {};
477 
478         var store = this.lookupStores[columnName];
479         if(!store)
480         {
481             //find the column metadata
482             var fieldMeta = this.findFieldMeta(columnName);
483             if(!fieldMeta)
484                 return null;
485 
486             //create the lookup store and kick off a load
487             var config = {
488                 schemaName: fieldMeta.lookup.schema,
489                 queryName: fieldMeta.lookup.table,
490                 containerPath: fieldMeta.lookup.containerPath || this.containerPath
491             };
492             if(includeNullRecord)
493                 config.nullRecord = {
494                     displayColumn: fieldMeta.lookup.displayColumn,
495                     nullCaption: this.lookupNullCaption || "[none]"
496                 };
497 
498             store = new LABKEY.ext.Store(config);
499             this.lookupStores[columnName] = store;
500         }
501         return store;
502     },
503 
504     exportData : function(format) {
505         format = format || "excel";
506         if (this.sql)
507         {
508             var exportCfg = {
509                 schemaName: this.schemaName,
510                 sql: this.sql,
511                 format: format,
512                 containerPath: this.containerPath,
513                 containerFilter: this.containerFilter
514             };
515 
516             // TODO: Enable filtering on exportData
517 //            var filters = [];
518 //
519 //            // respect base filters
520 //            if (Ext.isArray(this.filterArray)) {
521 //                filters = this.filterArray;
522 //            }
523 //
524 //            // respect user/view filters
525 //            if (this.getUserFilters().length > 0) {
526 //                var userFilters = this.getUserFilters();
527 //                for (var f=0; f < userFilters.length; f++) {
528 //                    filters.push(userFilters[f]);
529 //                }
530 //            }
531 //
532 //            if (filters.length > 0) {
533 //                exportCfg['filterArray'] = filters;
534 //            }
535 
536             LABKEY.Query.exportSql(exportCfg);
537         }
538         else
539         {
540             var params = {
541                 schemaName: this.schemaName,
542                 "query.queryName": this.queryName,
543                 "query.containerFilterName": this.containerFilter,
544                 "query.showRows": 'all'
545             };
546 
547             if (this.columns) {
548                 params['query.columns'] = this.columns;
549             }
550 
551             // These are filters that are custom created (aka not from a defined view).
552             LABKEY.Filter.appendFilterParams(params, this.filterArray);
553             
554             if (this.sortInfo) {
555                 params['query.sort'] = "DESC" == this.sortInfo.direction
556                         ? "-" + this.sortInfo.field
557                         : this.sortInfo.field;
558             }
559 
560             // These are filters that are defined by the view.
561             LABKEY.Filter.appendFilterParams(params, this.getUserFilters());
562 
563             var action = ("tsv" == format) ? "exportRowsTsv" : "exportRowsExcel";
564             window.location = LABKEY.ActionURL.buildURL("query", action, this.containerPath, params);
565         }
566     },
567 
568     /*-- Private Methods --*/
569 
570     onCommitSuccess : function(response, options) {
571         var json = this.getJson(response);
572         if(!json || !json.result) {
573             return;
574         }
575 
576         for (var cmdIdx = 0; cmdIdx < json.result.length; ++cmdIdx)
577         {
578             this.processResponse(json.result[cmdIdx].rows);
579         }
580         this.fireEvent("commitcomplete");
581     },
582 
583     processResponse : function(rows){
584         var idCol = this.reader.jsonData.metaData.id;
585         var row;
586         var record;
587         for(var idx = 0; idx < rows.length; ++idx)
588         {
589             row = rows[idx];
590 
591             if(!row || !row.values)
592                 return;
593 
594             //find the record using the id sent to the server
595             record = this.getById(row.oldKeys[this.reader.meta.id]);
596             if(!record)
597                 return;
598 
599             //apply values from the result row to the sent record
600             for(var col in record.data)
601             {
602                 //since the sent record might contain columns form a related table,
603                 //ensure that a value was actually returned for that column before trying to set it
604                 if(undefined !== row.values[col]){
605                     var x = record.fields.get(col);
606                     record.set(col, record.fields.get(col).convert(row.values[col], row.values));
607                 }
608 
609                 //clear any displayValue there might be in the extended info
610                 if(record.json && record.json[col])
611                     delete record.json[col].displayValue;
612             }
613 
614             //if the id changed, fixup the keys and map of the store's base collection
615             //HACK: this is using private data members of the base Store class. Unfortunately
616             //Ext Store does not have a public API for updating the key value of a record
617             //after it has been added to the store. This might break in future versions of Ext.
618             if(record.id != row.values[idCol])
619             {
620                 record.id = row.values[idCol];
621                 this.data.keys[this.data.indexOf(record)] = row.values[idCol];
622 
623                 delete this.data.map[record.id];
624                 this.data.map[row.values[idCol]] = record;
625             }
626 
627             //reset transitory flags and commit the record to let
628             //bound controls know that it's now clean
629             delete record.saveOperationInProgress;
630             delete record.isNew;
631             record.commit();
632         }
633 
634     },
635 
636     getOnCommitFailure : function(records) {
637         return function(response, options) {
638             
639             for(var idx = 0; idx < records.length; ++idx)
640                 delete records[idx].saveOperationInProgress;
641 
642             var json = this.getJson(response);
643             var message = (json && json.exception) ? json.exception : response.statusText;
644 
645             if(false !== this.fireEvent("commitexception", message))
646                 Ext.Msg.alert("Error During Save", "Could not save changes due to the following error:\n" + message);
647         };
648     },
649 
650     getJson : function(response) {
651         return (response && undefined != response.getResponseHeader && undefined != response.getResponseHeader('Content-Type')
652                 && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0)
653                 ? Ext.util.JSON.decode(response.responseText)
654                 : null;
655     },
656 
657     findFieldMeta : function(columnName)
658     {
659         var fields = this.reader.meta.fields;
660         for(var idx = 0; idx < fields.length; ++idx)
661         {
662             if(fields[idx].name == columnName)
663                 return fields[idx];
664         }
665         return null;
666     },
667 
668     onBeforeRequest : function(connection, options) {
669         if (this.sql)
670         {
671             // need to adjust url
672             var qsParams = {};
673             if (options.params['query.sort'])
674                 qsParams['query.sort'] = options.params['query.sort'];
675 
676             options.url = LABKEY.ActionURL.buildURL("query", "executeSql.api", this.containerPath, qsParams);
677         }
678     },
679 
680     onBeforeLoad : function(store, options) {
681         this.isLoading = true;
682 
683         //the selectRows.api can't handle the 'sort' and 'dir' params
684         //sent by Ext, so translate them into the expected form
685         if(options.params && options.params.sort) {
686             options.params['query.sort'] = "DESC" == options.params.dir
687                     ? "-" + options.params.sort
688                     : options.params.sort;
689             delete options.params.sort;
690             delete options.params.dir;
691         }
692 
693         // respect base filters
694         var baseFilters = {};
695         if (Ext.isArray(this.filterArray)) {
696             LABKEY.Filter.appendFilterParams(baseFilters, this.filterArray);
697         }
698 
699         // respect user filters
700         var userFilters = {};
701         LABKEY.Filter.appendFilterParams(userFilters, this.getUserFilters());
702 
703         Ext.applyIf(baseFilters, userFilters);
704 
705         // remove all query filters in base parameters
706         for (var param in this.baseParams) {
707             if (this.isFilterParam(param)) {
708                 delete this.baseParams[param];
709             }
710         }
711 
712         for (param in baseFilters) {
713             if (baseFilters.hasOwnProperty(param)) {
714                 this.setBaseParam(param, baseFilters[param]);
715             }
716         }
717     },
718 
719     isFilterParam : function(param) {
720         if (Ext.isString(param)) {
721             if (param.indexOf('query.') == 0) {
722                 var chkParam = param.replace('query.', '');
723                 if (!this.queryParamSet[chkParam]) {
724                     return true;
725                 }
726             }
727         }
728         return false;
729     },
730 
731     onLoad : function(store, records, options) {
732         this.isLoading = false;
733 
734         //remember the name of the id column
735         this.idName = this.reader.meta.id;
736 
737         if(this.nullRecord)
738         {
739             //create an extra record with a blank id column
740             //and the null caption in the display column
741             var data = {};
742             data[this.reader.meta.id] = "";
743             data[this.nullRecord.displayColumn] = this.nullRecord.nullCaption || this.nullCaption || "[none]";
744 
745             var recordConstructor = Ext.data.Record.create(this.reader.meta.fields);
746             var record = new recordConstructor(data, -1);
747             this.insert(0, record);
748         }
749     },
750 
751     onLoadException : function(proxy, options, response, error)
752     {
753         this.isLoading = false;
754         var loadError = {message: error};
755 
756         if(response && response.getResponseHeader
757                 && response.getResponseHeader("Content-Type").indexOf("application/json") >= 0)
758         {
759             var errorJson = Ext.util.JSON.decode(response.responseText);
760             if(errorJson && errorJson.exception)
761                 loadError.message = errorJson.exception;
762         }
763 
764         this.loadError = loadError;
765     },
766 
767     onUpdate : function(store, record, operation)
768     {
769         for(var field  in record.getChanges())
770         {
771             if(record.json && record.json[field])
772             {
773                 delete record.json[field].displayValue;
774                 delete record.json[field].mvValue;
775             }
776         }
777     },
778 
779     getDeleteSuccessHandler : function() {
780         var store = this;
781         return function(results) {
782             store.fireEvent("commitcomplete");
783             store.reload();
784         };
785     },
786 
787     getRowData : function(record) {
788         //need to convert empty strings to null before posting
789         //Ext components will typically set a cleared field to
790         //empty string, but this messes-up Lists in LabKey 8.2 and earlier
791         var data = {};
792         Ext.apply(data, record.data);
793         for(var field in data)
794         {
795             if(null != data[field] && data[field].toString().length == 0)
796                 data[field] = null;
797         }
798         return data;
799     },
800 
801     getOldKeys : function(record) {
802         var oldKeys = {};
803         oldKeys[this.reader.meta.id] = record.id;
804         return oldKeys;
805     },
806 
807     readyForSave : function(record) {
808         //this is kind of hacky, but it seems that checking
809         //for required columns is the job of the store, not
810         //the bound control. Since the required prop is in the
811         //column model, go get that from the reader
812         if (this.noValidationCheck)
813             return true;
814 
815         var colmodel = this.reader.jsonData.columnModel;
816         if(!colmodel)
817             return true;
818 
819         var col;
820         for(var idx = 0; idx < colmodel.length; ++idx)
821         {
822             col = colmodel[idx];
823 
824             if(col.dataIndex != this.reader.meta.id && col.required && !record.data[col.dataIndex])
825                 return false;
826         }
827 
828         return true;
829     },
830 
831     getUserFilters: function()
832     {
833         return this.userFilters || [];
834     },
835 
836     setUserFilters: function(filters)
837     {
838         this.userFilters = filters;
839     },
840 
841     getSql : function ()
842     {
843         return this.sql;
844     },
845 
846     setSql : function (sql)
847     {
848         this.sql = sql;
849         this.setBaseParam("sql", sql);
850     }
851 });
852 Ext.reg("labkey-store", LABKEY.ext.Store);
853 
854