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) 2012-2019 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