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