1 /* 2 * Copyright (c) 2012-2016 LabKey Corporation 3 * 4 * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 5 */ 6 7 /** 8 * Constructs an extended ExtJS 4.2.1 Ext.data.Store configured for use in LabKey client-side applications. 9 * @name LABKEY.ext4.data.Store 10 * @class 11 * LabKey extension to the <a href="http://docs.sencha.com/ext-js/4-0/#!/api/Ext.data.Store">Ext.data.Store</a> class, 12 * which can retrieve data from a LabKey server, track changes, and update the server upon demand. This is most typically 13 * used with data-bound user interface widgets, such as the Ext.grid.Panel. 14 * 15 * <p>If you use any of the LabKey APIs that extend Ext APIs, you must either make your code open source or 16 * <a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=extDevelopment">purchase an Ext license</a>.</p> 17 * <p>Additional Documentation: 18 * <ul> 19 * <li><a href="https://www.labkey.org/wiki/home/Documentation/page.view?name=javascriptTutorial">Tutorial: Create Applications with the JavaScript API</a></li> 20 * </ul> 21 * </p> 22 * @augments Ext.data.Store 23 * @param config Configuration properties. 24 * @param {String} config.schemaName The LabKey schema to query. 25 * @param {String} config.queryName The query name within the schema to fetch. 26 * @param {String} [config.sql] A LabKey SQL statement to execute to fetch the data. You may specify either a queryName or sql, 27 * 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. 28 * @param {String} [config.viewName] A saved custom view of the specified query to use if desired. 29 * @param {String} [config.columns] A comma-delimited list of column names to fetch from the specified query. Note 30 * that the names may refer to columns in related tables using the form 'column/column/column' (e.g., 'RelatedPeptide/TrimmedPeptide'). 31 * @param {String} [config.sort] A base sort specification in the form of '[-]column,[-]column' ('-' is used for descending sort). 32 * @param {Array} [config.filterArray] An array of LABKEY.Filter.FilterDefinition objects to use as the base filters. 33 * @param {Boolean} [config.updatable] Defaults to true. Set to false to prohibit updates to this store. 34 * @param {String} [config.containerPath] The container path from which to get the data. If not specified, the current container is used. 35 * @param {Integer} [config.maxRows] The maximum number of rows returned by this query (defaults to showing all rows). 36 * @param {Boolean} [config.ignoreFilter] True will ignore any filters applied as part of the view (defaults to false). 37 * @param {String} [config.containerFilter] The container filter to use for this query (defaults to null). 38 * Supported values include: 39 * <ul> 40 * <li>"Current": Include the current folder only</li> 41 * <li>"CurrentAndSubfolders": Include the current folder and all subfolders</li> 42 * <li>"CurrentPlusProject": Include the current folder and the project that contains it</li> 43 * <li>"CurrentAndParents": Include the current folder and its parent folders</li> 44 * <li>"CurrentPlusProjectAndShared": Include the current folder plus its project plus any shared folders</li> 45 * <li>"AllFolders": Include all folders for which the user has read permission</li> 46 * </ul> 47 * @param {Object} [config.metadata] A metadata object that will be applied to the default metadata returned by the server. See example below for usage. 48 * @param {Object} [config.metadataDefaults] A metadata object that will be applied to every field of the default metadata returned by the server. Will be superceeded by the metadata object in case of conflicts. See example below for usage. 49 * @param {boolean} [config.supressErrorAlert] If true, no dialog will appear if there is an exception. Defaults to false. 50 * 51 * @example <script type="text/javascript"> 52 var _store; 53 54 Ext4.onReady(function(){ 55 56 // create a Store bound to the 'Users' list in the 'core' schema 57 _store = Ext4.create('LABKEY.ext4.data.Store', { 58 schemaName: 'core', 59 queryName: 'users', 60 autoLoad: true 61 }); 62 }); 63 64 </script> 65 <div id='grid'/> 66 */ 67 68 Ext4.define('LABKEY.ext4.data.Store', { 69 70 extend: 'Ext.data.Store', 71 alternateClassName: 'LABKEY.ext4.Store', 72 73 alias: ['store.labkeystore', 'store.labkey-store'], 74 75 //the page size defaults to 25, which can give odd behavior for combos or other applications. 76 //applications that want to use paging should modify this. 100K matches the implicit client API pagesize 77 pageSize: 100000, 78 79 constructor: function(config) { 80 config = config || {}; 81 82 config.updatable = Ext4.isDefined(config.updatable) ? config.updatable : true; 83 84 var baseParams = this.generateBaseParams(config); 85 86 Ext4.apply(this, config); 87 88 //specify an empty fields array instead of a model. the reader will creates a model later 89 this.fields = []; 90 91 this.proxy = this.getProxyConfig(); 92 93 //see note below 94 var autoLoad = config.autoLoad; 95 config.autoLoad = false; 96 this.autoLoad = false; 97 this.loading = autoLoad; //allows combos to properly set initial value w/ asyc store load 98 99 // call the superclass's constructor 100 this.callParent([config]); 101 102 //NOTE: if the config object contains a load lister it will be executed prior to this one...not sure if that's a problem or not 103 this.on('beforeload', this.onBeforeLoad, this); 104 this.on('load', this.onLoad, this); 105 this.on('update', this.onStoreUpdate, this); 106 this.on('add', this.onAdd, this); 107 108 this.proxy.reader.on('datachange', this.onReaderLoad, this); 109 110 //Add this here instead of allowing Ext.store to autoLoad to make sure above listeners are added before 1st load 111 if(autoLoad){ 112 this.autoLoad = autoLoad; 113 Ext4.defer(this.load, 10, this, [ 114 typeof this.autoLoad == 'object' ? this.autoLoad : undefined 115 ]); 116 } 117 118 /** 119 * @memberOf LABKEY.ext4.data.Store# 120 * @name beforemetachange 121 * @event 122 * @description Fired when the initial query metadata is returned from the server. Provides an opportunity to manipulate it. 123 * @param {Object} store A reference to the LABKEY store 124 * @param {Object} metadata The metadata object that will be supplied to the Ext.data.Model. 125 */ 126 127 /** 128 * @memberOf LABKEY.ext4.data.Store# 129 * @name exception 130 * @event 131 * @description Fired when there is an exception loading or saving data. 132 * @param {Object} store A reference to the LABKEY store 133 * @param {String} message The error message 134 * @param {Object} response The response object 135 * @param {Object} operation The Ext.data.Operation object 136 */ 137 138 /** 139 * @memberOf LABKEY.ext4.data.Store# 140 * @name synccomplete 141 * @event 142 * @description Fired when a sync operation is complete, which can include insert/update/delete events 143 * @param {Object} store A reference to the LABKEY store 144 */ 145 this.addEvents('beforemetachange', 'exception', 'synccomplete'); 146 }, 147 148 //private 149 getProxyConfig: function(){ 150 return { 151 type: 'labkeyajax', 152 store: this, 153 timeout: this.timeout, 154 listeners: { 155 scope: this, 156 exception: this.onProxyException 157 }, 158 extraParams: this.generateBaseParams() 159 } 160 }, 161 162 generateBaseParams: function(config){ 163 if (config) 164 this.initialConfig = Ext4.apply({}, config); 165 166 config = config || this; 167 var baseParams = {}; 168 baseParams.schemaName = config.schemaName; 169 baseParams.apiVersion = 9.1; 170 171 if (config.parameters) { 172 Ext4.iterate(config.parameters, function(param, value) { 173 baseParams['query.param.' + param] = value; 174 }); 175 } 176 177 if (config.containerFilter){ 178 //baseParams['query.containerFilterName'] = config.containerFilter; 179 baseParams['containerFilter'] = config.containerFilter; 180 } 181 182 if (config.ignoreFilter) 183 baseParams['query.ignoreFilter'] = 1; 184 185 if (Ext4.isDefined(config.maxRows)){ 186 baseParams['query.maxRows'] = config.maxRows; 187 if (config.maxRows < this.pageSize) 188 this.pageSize = config.maxRows; 189 190 if (config.maxRows === 0) 191 this.pageSize = 0; 192 } 193 194 if (config.viewName) 195 baseParams['query.viewName'] = config.viewName; 196 197 if (config.columns) 198 baseParams['query.columns'] = Ext4.isArray(config.columns) ? config.columns.join(",") : config.columns; 199 200 if (config.queryName) 201 baseParams['query.queryName'] = config.queryName; 202 203 if (config.containerPath) 204 baseParams.containerPath = config.containerPath; 205 206 if (config.pageSize && config.maxRows !== 0 && this.maxRows !== 0) 207 baseParams['limit'] = config.pageSize; 208 209 //NOTE: sort() is a method in the store. it's awkward to support a param, but we do it since selectRows() uses it 210 if (this.initialConfig && this.initialConfig.sort) 211 baseParams['query.sort'] = this.initialConfig.sort; 212 delete config.sort; //important...otherwise the native sort() method is overridden 213 214 if (config.sql){ 215 baseParams.sql = config.sql; 216 this.updatable = false; 217 } 218 else { 219 this.updatable = true; 220 } 221 222 LABKEY.Filter.appendFilterParams(baseParams, config.filterArray); 223 224 return baseParams; 225 }, 226 227 //private 228 //NOTE: the purpose of this is to provide a way to modify the server-supplied metadata and supplement with a client-supplied object 229 onReaderLoad: function(meta){ 230 //this.model.prototype.idProperty = this.proxy.reader.idProperty; 231 232 if (meta.fields && meta.fields.length){ 233 var fields = []; 234 Ext4.each(meta.fields, function(f){ 235 this.translateMetadata(f); 236 237 if (this.metadataDefaults){ 238 Ext4.Object.merge(f, this.metadataDefaults); 239 } 240 241 if (this.metadata){ 242 //allow more complex metadata, per field 243 if (this.metadata[f.name]){ 244 Ext4.Object.merge(f, this.metadata[f.name]); 245 } 246 } 247 248 fields.push(f.name); 249 }, this); 250 251 if (meta.title) 252 this.queryTitle = meta.title; 253 254 //allow mechanism to add new fields via metadata 255 if (this.metadata){ 256 var field; 257 for (var i in this.metadata){ 258 field = this.metadata[i]; 259 //TODO: we should investigate how convert() works and probably use this instead 260 if (field.createIfDoesNotExist && Ext4.Array.indexOf(i)==-1){ 261 field.name = field.name || i; 262 field.notFromServer = true; 263 this.translateMetadata(field); 264 if (this.metadataDefaults) 265 Ext4.Object.merge(field, this.metadataDefaults); 266 267 meta.fields.push(Ext4.apply({}, field)); 268 } 269 } 270 } 271 this.fireEvent('beforemetachange', this, meta); 272 } 273 }, 274 275 //private 276 translateMetadata: function(field){ 277 LABKEY.ext4.Util.translateMetadata(field); 278 }, 279 280 //private 281 setModel: function(model){ 282 // NOTE: if the query lacks a PK, which can happen with queries that dont represent physical tables, 283 // Ext adds a column to hold an Id. In order to differentiate this from other fields we set defaults 284 this.model.prototype.fields.each(function(field){ 285 if (field.name == '_internalId'){ 286 Ext4.apply(field, { 287 hidden: true, 288 calculatedField: true, 289 shownInInsertView: false, 290 shownInUpdateView: false, 291 userEditable: false 292 }); 293 } 294 }); 295 this.model = model; 296 this.implicitModel = false; 297 }, 298 299 //private 300 load: function(){ 301 this.generateBaseParams(); 302 this.proxy.on('exception', this.onProxyException, this, {single: true}); 303 return this.callParent(arguments); 304 }, 305 306 //private 307 sync: function(){ 308 this.generateBaseParams(); 309 310 if (!this.updatable){ 311 alert('This store is not updatable'); 312 return; 313 } 314 315 if (!this.syncNeeded()){ 316 this.fireEvent('synccomplete', this); 317 return; 318 } 319 320 this.proxy.on('exception', this.onProxyException, this, {single: true}); 321 return this.callParent(arguments); 322 }, 323 324 //private 325 update: function(){ 326 this.generateBaseParams(); 327 328 if (!this.updatable){ 329 alert('This store is not updatable'); 330 return; 331 } 332 return this.callParent(arguments); 333 }, 334 335 //private 336 create: function(){ 337 this.generateBaseParams(); 338 339 if (!this.updatable){ 340 alert('This store is not updatable'); 341 return; 342 } 343 return this.callParent(arguments); 344 }, 345 346 //private 347 destroy: function(){ 348 this.generateBaseParams(); 349 350 if (!this.updatable){ 351 alert('This store is not updatable'); 352 return; 353 } 354 return this.callParent(arguments); 355 }, 356 357 /** 358 * Returns the case-normalized fieldName. The fact that field names are not normally case-sensitive, but javascript is case-sensitive can cause prolems. This method is designed to allow you to convert a string into the casing used by the store. 359 * @name getCanonicalFieldName 360 * @function 361 * @param {String} fieldName The name of the field to test 362 * @returns {String} The normalized field name or null if not found 363 * @memberOf LABKEY.ext4.data.Store# 364 */ 365 getCanonicalFieldName: function(fieldName){ 366 var fields = this.getFields(); 367 if (fields.get(fieldName)){ 368 return fieldName; 369 } 370 371 var name; 372 373 var properties = ['name', 'fieldKeyPath']; 374 Ext4.each(properties, function(prop){ 375 fields.each(function(field){ 376 if (field[prop].toLowerCase() == fieldName.toLowerCase()){ 377 name = field.name; 378 return false; 379 } 380 }); 381 382 if (name) 383 return false; //abort the loop 384 }, this); 385 386 return name; 387 }, 388 389 //private 390 //NOTE: the intent of this is to allow fields to have an initial value defined through a function. see getInitialValue in LABKEY.ext4.Util.getDefaultEditorConfig 391 onAdd: function(store, records, idx, opts){ 392 var val, record; 393 this.getFields().each(function(meta){ 394 if (meta.getInitialValue){ 395 for (var i=0;i<records.length;i++){ 396 record = records[i]; 397 val = meta.getInitialValue(record.get(meta.name), record, meta); 398 record.set(meta.name, val); 399 } 400 } 401 }, this); 402 }, 403 404 //private 405 onBeforeLoad: function(operation){ 406 if (this.sql){ 407 operation.sql = this.sql; 408 } 409 this.proxy.containerPath = this.containerPath; 410 this.proxy.extraParams = this.generateBaseParams(); 411 }, 412 413 //private 414 //NOTE: maybe this should be a plugin to combos?? 415 onLoad : function(store, records, success) { 416 if (!success) 417 return; 418 //the intent is to let the client set default values for created fields 419 var toUpdate = []; 420 this.getFields().each(function(f){ 421 if (f.setValueOnLoad && (f.getInitialValue || f.defaultValue)) 422 toUpdate.push(f); 423 }, this); 424 if (toUpdate.length){ 425 var allRecords = this.getRange(); 426 for (var i=0;i<allRecords.length;i++){ 427 var rec = allRecords[i]; 428 for (var j=0;j<toUpdate.length;j++){ 429 var meta = toUpdate[j]; 430 if (meta.getInitialValue) 431 rec.set(meta.name, meta.getInitialValue(rec.get(meta.name), rec, meta)); 432 else if (meta.defaultValue && !rec.get(meta.name)) 433 rec.set(meta.name, meta.defaultValue) 434 } 435 } 436 } 437 }, 438 439 onProxyWrite: function(operation) { 440 var me = this, 441 success = operation.wasSuccessful(), 442 records = operation.getRecords(); 443 444 switch (operation.action) { 445 case 'saveRows': 446 me.onSaveRows(operation, success); 447 break; 448 default: 449 console.log('something other than saveRows happened: ' + operation.action) 450 } 451 452 if (success) { 453 me.fireEvent('write', me, operation); 454 me.fireEvent('datachanged', me); 455 } 456 //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional 457 Ext4.callback(operation.callback, operation.scope || me, [records, operation, success]); 458 459 //NOTE: this was created to give a single event to follow, regardless of success 460 this.fireEvent('synccomplete', this, operation, success); 461 }, 462 463 //private 464 processResponse : function(rows){ 465 var idCol = this.proxy.reader.getIdProperty(); 466 var row; 467 var record; 468 var index; 469 for (var idx = 0; idx < rows.length; ++idx) 470 { 471 row = rows[idx]; 472 473 //an example row in this situation would be the response from a delete command 474 if (!row || !row.values) 475 return; 476 477 //find the record using the id sent to the server 478 record = this.getById(row.oldKeys[idCol]); 479 480 //records created client-side might not have a PK yet, so we try to use internalId to find it 481 //we defer to snapshot, since this will contain all records, even if the store is filtered 482 if (!record) 483 record = (this.snapshot || this.data).get(row.oldKeys['_internalId']); 484 485 if (!record) 486 return; 487 488 //apply values from the result row to the sent record 489 for (var col in record.data) 490 { 491 //since the sent record might contain columns form a related table, 492 //ensure that a value was actually returned for that column before trying to set it 493 if (undefined !== row.values[col]){ 494 record.set(col, record.fields.get(col).convert(row.values[col], row.values)); 495 } 496 497 //clear any displayValue there might be in the extended info 498 if (record.json && record.json[col]) 499 delete record.json[col].displayValue; 500 } 501 502 //if the id changed, fixup the keys and map of the store's base collection 503 //HACK: this is using private data members of the base Store class. Unfortunately 504 //Ext Store does not have a public API for updating the key value of a record 505 //after it has been added to the store. This might break in future versions of Ext 506 if (record.internalId != row.values[idCol]) 507 { 508 //ISSUE 22289: we need to find the original index before changing the internalId, or the record will not get found 509 index = this.data.indexOf(record); 510 record.internalId = row.values[idCol]; 511 record.setId(row.values[idCol]); 512 if (index > -1) { 513 this.data.removeAt(index); 514 this.data.insert(index, record); 515 } 516 } 517 518 //reset transitory flags and commit the record to let 519 //bound controls know that it's now clean 520 delete record.saveOperationInProgress; 521 522 record.phantom = false; 523 record.commit(); 524 } 525 }, 526 527 //private 528 getJson : function(response) { 529 return (response && undefined != response.getResponseHeader && undefined != response.getResponseHeader('Content-Type') 530 && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0) 531 ? Ext4.JSON.decode(response.responseText) 532 : null; 533 }, 534 535 //private 536 onSaveRows: function(operation, success){ 537 var json = this.getJson(operation.response); 538 if (!json || !json.result) 539 return; 540 541 for (var commandIdx = 0; commandIdx < json.result.length; ++commandIdx) 542 { 543 this.processResponse(json.result[commandIdx].rows); 544 } 545 }, 546 547 //private 548 onProxyException : function(proxy, response, operation, eOpts) { 549 var loadError = {message: response.statusText}; 550 var json = this.getJson(response); 551 552 if (json){ 553 if (json && json.exception) 554 loadError.message = json.exception; 555 556 response.errors = json; 557 558 this.processErrors(json); 559 } 560 561 this.loadError = loadError; 562 563 //TODO: is this the right behavior? 564 if (response && (response.status === 200 || response.status == 0)){ 565 return; 566 } 567 568 var message = (json && json.exception) ? json.exception : response.statusText; 569 570 var messageBody; 571 switch(operation.action){ 572 case 'read': 573 messageBody = 'Could not load records'; 574 break; 575 case 'saveRows': 576 messageBody = 'Could not save records'; 577 break; 578 default: 579 messageBody = 'There was an error'; 580 } 581 582 if (message) 583 messageBody += ' due to the following error:' + "<br>" + message; 584 else 585 messageBody += ' due to an unexpected error'; 586 587 if (false !== this.fireEvent("exception", this, messageBody, response, operation)){ 588 589 if (!this.supressErrorAlert) 590 Ext4.Msg.alert("Error", messageBody); 591 592 console.log(response); 593 } 594 }, 595 596 processErrors: function(json){ 597 Ext4.each(json.errors, function(error){ 598 //the error object for 1 row. 1-based row numbering 599 if (Ext4.isDefined(error.rowNumber)){ 600 var record = this.getAt(error.rowNumber - 1); 601 if (!record) 602 return; 603 604 record.serverErrors = {}; 605 606 Ext4.each(error.errors, function(e){ 607 if (!record.serverErrors[e.field]) 608 record.serverErrors[e.field] = []; 609 610 if (record.serverErrors[e.field].indexOf(e.message) == -1) 611 record.serverErrors[e.field].push(e.message); 612 }, this); 613 } 614 }, this); 615 }, 616 617 //private 618 // NOTE: these values are returned by the store in the 9.1 API format 619 // They provide the display value and information used in Missing value indicators 620 // They are used by the Ext grid when rendering or creating a tooltip. They are deleted here prsumably b/c if the value 621 // is changed then we cannot count on them being accurate 622 onStoreUpdate : function(store, record, operation) { 623 for (var field in record.getChanges()){ 624 if (record.raw && record.raw[field]){ 625 delete record.raw[field].displayValue; 626 delete record.raw[field].mvValue; 627 } 628 } 629 }, 630 631 syncNeeded: function(){ 632 return this.getNewRecords().length > 0 || 633 this.getUpdatedRecords().length > 0 || 634 this.getRemovedRecords().length > 0 635 }, 636 637 /** 638 * @private 639 * Using the store's metadata, this method returns an Ext config object suitable for creating an Ext field. 640 * The resulting object is configured to be used in a form, as opposed to a grid. 641 * This is a convenience wrapper around LABKEY.ext4.Util.getFormEditorConfig 642 * <p> 643 * For information on using metadata, see LABKEY.ext4.Util 644 * 645 * @name getFormEditorConfig 646 * @function 647 * @param (string) fieldName The name of the field 648 * @param (object) config Optional. This object will be recursively applied to the default config object 649 * @returns {object} An Ext config object suitable to create a field component 650 */ 651 getFormEditorConfig: function(fieldName, config){ 652 var meta = this.findFieldMetadata(fieldName); 653 return LABKEY.ext4.Util.getFormEditorConfig(meta, config); 654 }, 655 656 /** 657 * @private 658 * Using the store's metadata, this method returns an Ext config object suitable for creating an Ext field. 659 * The resulting object is configured to be used in a grid, as opposed to a form. 660 * This is a convenience wrapper around LABKEY.ext4.Util.getGridEditorConfig 661 * <p> 662 * For information on using metadata, see LABKEY.ext4.Util 663 * @name getGridEditorConfig 664 * @function 665 * @param (string) fieldName The name of the field 666 * @param (object) config Optional. This object will be recursively applied to the default config object 667 * @returns {object} An Ext config object suitable to create a field component 668 */ 669 getGridEditorConfig: function(fieldName, config){ 670 var meta = this.findFieldMetadata(fieldName); 671 return LABKEY.ext4.Util.getGridEditorConfig(meta, config); 672 }, 673 674 /** 675 * Returns an Ext.util.MixedCollection containing the fields associated with this store 676 * 677 * @name getFields 678 * @function 679 * @returns {Ext.util.MixedCollection} The fields associated with this store 680 * @memberOf LABKEY.ext4.data.Store# 681 * 682 */ 683 getFields: function(){ 684 return this.proxy.reader.model.prototype.fields; 685 }, 686 687 /** 688 * Returns an array of the raw column objects returned from the server along with the query metadata 689 * 690 * @name getColumns 691 * @function 692 * @returns {Array} The columns associated with this store 693 * @memberOf LABKEY.ext4.data.Store# 694 * 695 */ 696 getColumns: function(){ 697 return this.proxy.reader.rawData.columnModel; 698 }, 699 700 /** 701 * Returns a field metadata object of the specified field 702 * 703 * @name findFieldMetadata 704 * @function 705 * @param {String} fieldName The name of the field 706 * @returns {Object} Metatdata for this field 707 * @memberOf LABKEY.ext4.data.Store# 708 * 709 */ 710 findFieldMetadata : function(fieldName){ 711 var fields = this.getFields(); 712 if (!fields) 713 return null; 714 715 return fields.get(fieldName); 716 }, 717 718 exportData : function(format) { 719 format = format || "excel"; 720 if (this.sql) 721 { 722 LABKEY.Query.exportSql({ 723 schemaName: this.schemaName, 724 sql: this.sql, 725 format: format, 726 containerPath: this.containerPath, 727 containerFilter: this.containerFilter 728 }); 729 } 730 else 731 { 732 var config = this.getExportConfig(format); 733 window.location = config.url; 734 } 735 }, 736 737 getExportConfig : function(format) { 738 739 format = format || "excel"; 740 741 var params = { 742 schemaName: this.schemaName, 743 "query.queryName": this.queryName, 744 "query.containerFilterName": this.containerFilter 745 }; 746 747 if (this.columns) { 748 params["query.columns"] = Ext4.isArray(this.columns) ? this.columns.join(',') : this.columns; 749 } 750 751 // These are filters that are custom created (aka not from a defined view). 752 LABKEY.Filter.appendFilterParams(params, this.filterArray); 753 754 if (this.sortInfo) { 755 params["query.sort"] = ("DESC" === this.sortInfo.direction ? "-" : "") + this.sortInfo.field; 756 } 757 758 var config = { 759 action: ("tsv" === format) ? "exportRowsTsv" : "exportRowsExcel", 760 params: params 761 }; 762 763 config.url = LABKEY.ActionURL.buildURL("query", config.action, this.containerPath, config.params); 764 765 return config; 766 }, 767 768 //Ext3 compatability?? 769 commitChanges: function(){ 770 this.sync(); 771 }, 772 773 //private 774 getKeyField: function(){ 775 return this.model.prototype.idProperty; 776 }, 777 778 //private, experimental 779 getQueryConfig: function(){ 780 return { 781 containerPath: this.containerPath, 782 schemaName: this.schemaName, 783 queryName: this.queryName, 784 viewName: this.viewName, 785 queryTitle: this.queryTitle, 786 sql: this.sql, 787 columns: this.columns, 788 filterArray: this.filterArray, 789 sort: this.initialConfig.sort, 790 maxRows: this.maxRows, 791 containerFilter: this.containerFilter 792 } 793 } 794 795 });