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