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