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