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/Documentation/wiki-page.view?name=extDevelopment">purchase an Ext license</a>.</p> 17 * <p>Additional Documentation: 18 * <ul> 19 * <li><a href="https://www.labkey.org/Documentation/wiki-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 // Issue 32269 - force key and other non-requested columns to be sent back 171 baseParams.minimalColumns = false; 172 173 if (config.parameters) { 174 Ext4.iterate(config.parameters, function(param, value) { 175 baseParams['query.param.' + param] = value; 176 }); 177 } 178 179 if (config.containerFilter){ 180 //baseParams['query.containerFilterName'] = config.containerFilter; 181 baseParams['containerFilter'] = config.containerFilter; 182 } 183 184 if (config.ignoreFilter) 185 baseParams['query.ignoreFilter'] = 1; 186 187 if (Ext4.isDefined(config.maxRows)){ 188 baseParams['query.maxRows'] = config.maxRows; 189 if (config.maxRows < this.pageSize) 190 this.pageSize = config.maxRows; 191 192 if (config.maxRows === 0) 193 this.pageSize = 0; 194 } 195 196 if (config.viewName) 197 baseParams['query.viewName'] = config.viewName; 198 199 if (config.columns) 200 baseParams['query.columns'] = Ext4.isArray(config.columns) ? config.columns.join(",") : config.columns; 201 202 if (config.queryName) 203 baseParams['query.queryName'] = config.queryName; 204 205 if (config.containerPath) 206 baseParams.containerPath = config.containerPath; 207 208 if (config.pageSize && config.maxRows !== 0 && this.maxRows !== 0) 209 baseParams['limit'] = config.pageSize; 210 211 //NOTE: sort() is a method in the store. it's awkward to support a param, but we do it since selectRows() uses it 212 if (this.initialConfig && this.initialConfig.sort) 213 baseParams['query.sort'] = this.initialConfig.sort; 214 delete config.sort; //important...otherwise the native sort() method is overridden 215 216 if (config.sql){ 217 baseParams.sql = config.sql; 218 this.updatable = false; 219 } 220 else { 221 this.updatable = true; 222 } 223 224 LABKEY.Filter.appendFilterParams(baseParams, config.filterArray); 225 226 return baseParams; 227 }, 228 229 //private 230 //NOTE: the purpose of this is to provide a way to modify the server-supplied metadata and supplement with a client-supplied object 231 onReaderLoad: function(meta){ 232 //this.model.prototype.idProperty = this.proxy.reader.idProperty; 233 234 if (meta.fields && meta.fields.length){ 235 var fields = []; 236 Ext4.each(meta.fields, function(f){ 237 this.translateMetadata(f); 238 239 if (this.metadataDefaults){ 240 Ext4.Object.merge(f, this.metadataDefaults); 241 } 242 243 if (this.metadata){ 244 //allow more complex metadata, per field 245 if (this.metadata[f.name]){ 246 Ext4.Object.merge(f, this.metadata[f.name]); 247 } 248 } 249 250 fields.push(f.name); 251 }, this); 252 253 if (meta.title) 254 this.queryTitle = meta.title; 255 256 //allow mechanism to add new fields via metadata 257 if (this.metadata){ 258 var field; 259 for (var i in this.metadata){ 260 field = this.metadata[i]; 261 //TODO: we should investigate how convert() works and probably use this instead 262 if (field.createIfDoesNotExist && Ext4.Array.indexOf(i)==-1){ 263 field.name = field.name || i; 264 field.notFromServer = true; 265 this.translateMetadata(field); 266 if (this.metadataDefaults) 267 Ext4.Object.merge(field, this.metadataDefaults); 268 269 meta.fields.push(Ext4.apply({}, field)); 270 } 271 } 272 } 273 this.fireEvent('beforemetachange', this, meta); 274 } 275 }, 276 277 //private 278 translateMetadata: function(field){ 279 LABKEY.ext4.Util.translateMetadata(field); 280 }, 281 282 //private 283 setModel: function(model){ 284 // NOTE: if the query lacks a PK, which can happen with queries that dont represent physical tables, 285 // Ext adds a column to hold an Id. In order to differentiate this from other fields we set defaults 286 this.model.prototype.fields.each(function(field){ 287 if (field.name == '_internalId'){ 288 Ext4.apply(field, { 289 hidden: true, 290 calculatedField: true, 291 shownInInsertView: false, 292 shownInUpdateView: false, 293 userEditable: false 294 }); 295 } 296 }); 297 this.model = model; 298 this.implicitModel = false; 299 }, 300 301 //private 302 load: function(){ 303 this.generateBaseParams(); 304 this.proxy.on('exception', this.onProxyException, this, {single: true}); 305 return this.callParent(arguments); 306 }, 307 308 //private 309 sync: function(){ 310 this.generateBaseParams(); 311 312 if (!this.updatable){ 313 alert('This store is not updatable'); 314 return; 315 } 316 317 if (!this.syncNeeded()){ 318 this.fireEvent('synccomplete', this); 319 return; 320 } 321 322 this.proxy.on('exception', this.onProxyException, this, {single: true}); 323 return this.callParent(arguments); 324 }, 325 326 //private 327 update: function(){ 328 this.generateBaseParams(); 329 330 if (!this.updatable){ 331 alert('This store is not updatable'); 332 return; 333 } 334 return this.callParent(arguments); 335 }, 336 337 //private 338 create: function(){ 339 this.generateBaseParams(); 340 341 if (!this.updatable){ 342 alert('This store is not updatable'); 343 return; 344 } 345 return this.callParent(arguments); 346 }, 347 348 //private 349 destroy: function(){ 350 this.generateBaseParams(); 351 352 if (!this.updatable){ 353 alert('This store is not updatable'); 354 return; 355 } 356 return this.callParent(arguments); 357 }, 358 359 /** 360 * 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. 361 * @name getCanonicalFieldName 362 * @function 363 * @param {String} fieldName The name of the field to test 364 * @returns {String} The normalized field name or null if not found 365 * @memberOf LABKEY.ext4.data.Store# 366 */ 367 getCanonicalFieldName: function(fieldName){ 368 var fields = this.getFields(); 369 if (fields.get(fieldName)){ 370 return fieldName; 371 } 372 373 var name; 374 375 var properties = ['name', 'fieldKeyPath']; 376 Ext4.each(properties, function(prop){ 377 fields.each(function(field){ 378 if (field[prop].toLowerCase() == fieldName.toLowerCase()){ 379 name = field.name; 380 return false; 381 } 382 }); 383 384 if (name) 385 return false; //abort the loop 386 }, this); 387 388 return name; 389 }, 390 391 //private 392 //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 393 onAdd: function(store, records, idx, opts){ 394 var val, record; 395 this.getFields().each(function(meta){ 396 if (meta.getInitialValue){ 397 for (var i=0;i<records.length;i++){ 398 record = records[i]; 399 val = meta.getInitialValue(record.get(meta.name), record, meta); 400 record.set(meta.name, val); 401 } 402 } 403 }, this); 404 }, 405 406 //private 407 onBeforeLoad: function(operation){ 408 if (this.sql){ 409 operation.sql = this.sql; 410 } 411 this.proxy.containerPath = this.containerPath; 412 this.proxy.extraParams = this.generateBaseParams(); 413 }, 414 415 //private 416 //NOTE: maybe this should be a plugin to combos?? 417 onLoad : function(store, records, success) { 418 if (!success) 419 return; 420 //the intent is to let the client set default values for created fields 421 var toUpdate = []; 422 this.getFields().each(function(f){ 423 if (f.setValueOnLoad && (f.getInitialValue || f.defaultValue)) 424 toUpdate.push(f); 425 }, this); 426 if (toUpdate.length){ 427 var allRecords = this.getRange(); 428 for (var i=0;i<allRecords.length;i++){ 429 var rec = allRecords[i]; 430 for (var j=0;j<toUpdate.length;j++){ 431 var meta = toUpdate[j]; 432 if (meta.getInitialValue) 433 rec.set(meta.name, meta.getInitialValue(rec.get(meta.name), rec, meta)); 434 else if (meta.defaultValue && !rec.get(meta.name)) 435 rec.set(meta.name, meta.defaultValue) 436 } 437 } 438 } 439 }, 440 441 onProxyWrite: function(operation) { 442 var me = this, 443 success = operation.wasSuccessful(), 444 records = operation.getRecords(); 445 446 switch (operation.action) { 447 case 'saveRows': 448 me.onSaveRows(operation, success); 449 break; 450 default: 451 console.log('something other than saveRows happened: ' + operation.action) 452 } 453 454 if (success) { 455 me.fireEvent('write', me, operation); 456 me.fireEvent('datachanged', me); 457 } 458 //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional 459 Ext4.callback(operation.callback, operation.scope || me, [records, operation, success]); 460 461 //NOTE: this was created to give a single event to follow, regardless of success 462 this.fireEvent('synccomplete', this, operation, success); 463 }, 464 465 //private 466 processResponse : function(rows){ 467 var idCol = this.proxy.reader.getIdProperty(); 468 var row; 469 var record; 470 var index; 471 for (var idx = 0; idx < rows.length; ++idx) 472 { 473 row = rows[idx]; 474 475 //an example row in this situation would be the response from a delete command 476 if (!row || !row.values) 477 return; 478 479 //find the record using the id sent to the server 480 record = this.getById(row.oldKeys[idCol]); 481 482 //records created client-side might not have a PK yet, so we try to use internalId to find it 483 //we defer to snapshot, since this will contain all records, even if the store is filtered 484 if (!record) 485 record = (this.snapshot || this.data).get(row.oldKeys['_internalId']); 486 487 if (!record) 488 return; 489 490 //apply values from the result row to the sent record 491 for (var col in record.data) 492 { 493 //since the sent record might contain columns form a related table, 494 //ensure that a value was actually returned for that column before trying to set it 495 if (undefined !== row.values[col]){ 496 record.set(col, record.fields.get(col).convert(row.values[col], row.values)); 497 } 498 499 //clear any displayValue there might be in the extended info 500 if (record.json && record.json[col]) 501 delete record.json[col].displayValue; 502 } 503 504 //if the id changed, fixup the keys and map of the store's base collection 505 //HACK: this is using private data members of the base Store class. Unfortunately 506 //Ext Store does not have a public API for updating the key value of a record 507 //after it has been added to the store. This might break in future versions of Ext 508 if (record.internalId != row.values[idCol]) 509 { 510 //ISSUE 22289: we need to find the original index before changing the internalId, or the record will not get found 511 index = this.data.indexOf(record); 512 record.internalId = row.values[idCol]; 513 record.setId(row.values[idCol]); 514 if (index > -1) { 515 this.data.removeAt(index); 516 this.data.insert(index, record); 517 } 518 } 519 520 //reset transitory flags and commit the record to let 521 //bound controls know that it's now clean 522 delete record.saveOperationInProgress; 523 524 record.phantom = false; 525 record.commit(); 526 } 527 }, 528 529 //private 530 getJson : function(response) { 531 return (response && undefined != response.getResponseHeader && undefined != response.getResponseHeader('Content-Type') 532 && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0) 533 ? Ext4.JSON.decode(response.responseText) 534 : null; 535 }, 536 537 //private 538 onSaveRows: function(operation, success){ 539 var json = this.getJson(operation.response); 540 if (!json || !json.result) 541 return; 542 543 for (var commandIdx = 0; commandIdx < json.result.length; ++commandIdx) 544 { 545 this.processResponse(json.result[commandIdx].rows); 546 } 547 }, 548 549 //private 550 onProxyException : function(proxy, response, operation, eOpts) { 551 var loadError = {message: response.statusText}; 552 var json = this.getJson(response); 553 554 if (json){ 555 if (json && json.exception) 556 loadError.message = json.exception; 557 558 response.errors = json; 559 560 this.processErrors(json); 561 } 562 563 this.loadError = loadError; 564 565 //TODO: is this the right behavior? 566 if (response && (response.status === 200 || response.status == 0)){ 567 return; 568 } 569 570 var message = (json && json.exception) ? json.exception : response.statusText; 571 572 var messageBody; 573 switch(operation.action){ 574 case 'read': 575 messageBody = 'Could not load records'; 576 break; 577 case 'saveRows': 578 messageBody = 'Could not save records'; 579 break; 580 default: 581 messageBody = 'There was an error'; 582 } 583 584 if (message) 585 messageBody += ' due to the following error:' + "<br>" + message; 586 else 587 messageBody += ' due to an unexpected error'; 588 589 if (false !== this.fireEvent("exception", this, messageBody, response, operation)){ 590 591 if (!this.supressErrorAlert) 592 Ext4.Msg.alert("Error", messageBody); 593 594 console.log(response); 595 } 596 }, 597 598 processErrors: function(json){ 599 Ext4.each(json.errors, function(error){ 600 //the error object for 1 row. 1-based row numbering 601 if (Ext4.isDefined(error.rowNumber)){ 602 var record = this.getAt(error.rowNumber - 1); 603 if (!record) 604 return; 605 606 record.serverErrors = {}; 607 608 Ext4.each(error.errors, function(e){ 609 if (!record.serverErrors[e.field]) 610 record.serverErrors[e.field] = []; 611 612 if (record.serverErrors[e.field].indexOf(e.message) == -1) 613 record.serverErrors[e.field].push(e.message); 614 }, this); 615 } 616 }, this); 617 }, 618 619 //private 620 // NOTE: these values are returned by the store in the 9.1 API format 621 // They provide the display value and information used in Missing value indicators 622 // They are used by the Ext grid when rendering or creating a tooltip. They are deleted here prsumably b/c if the value 623 // is changed then we cannot count on them being accurate 624 onStoreUpdate : function(store, record, operation) { 625 for (var field in record.getChanges()){ 626 if (record.raw && record.raw[field]){ 627 delete record.raw[field].displayValue; 628 delete record.raw[field].mvValue; 629 } 630 } 631 }, 632 633 syncNeeded: function(){ 634 return this.getNewRecords().length > 0 || 635 this.getUpdatedRecords().length > 0 || 636 this.getRemovedRecords().length > 0 637 }, 638 639 /** 640 * @private 641 * Using the store's metadata, this method returns an Ext config object suitable for creating an Ext field. 642 * The resulting object is configured to be used in a form, as opposed to a grid. 643 * This is a convenience wrapper around LABKEY.ext4.Util.getFormEditorConfig 644 * <p> 645 * For information on using metadata, see LABKEY.ext4.Util 646 * 647 * @name getFormEditorConfig 648 * @function 649 * @param (string) fieldName The name of the field 650 * @param (object) config Optional. This object will be recursively applied to the default config object 651 * @returns {object} An Ext config object suitable to create a field component 652 */ 653 getFormEditorConfig: function(fieldName, config){ 654 var meta = this.findFieldMetadata(fieldName); 655 return LABKEY.ext4.Util.getFormEditorConfig(meta, config); 656 }, 657 658 /** 659 * @private 660 * Using the store's metadata, this method returns an Ext config object suitable for creating an Ext field. 661 * The resulting object is configured to be used in a grid, as opposed to a form. 662 * This is a convenience wrapper around LABKEY.ext4.Util.getGridEditorConfig 663 * <p> 664 * For information on using metadata, see LABKEY.ext4.Util 665 * @name getGridEditorConfig 666 * @function 667 * @param (string) fieldName The name of the field 668 * @param (object) config Optional. This object will be recursively applied to the default config object 669 * @returns {object} An Ext config object suitable to create a field component 670 */ 671 getGridEditorConfig: function(fieldName, config){ 672 var meta = this.findFieldMetadata(fieldName); 673 return LABKEY.ext4.Util.getGridEditorConfig(meta, config); 674 }, 675 676 /** 677 * Returns an Ext.util.MixedCollection containing the fields associated with this store 678 * 679 * @name getFields 680 * @function 681 * @returns {Ext.util.MixedCollection} The fields associated with this store 682 * @memberOf LABKEY.ext4.data.Store# 683 * 684 */ 685 getFields: function(){ 686 return this.proxy.reader.model.prototype.fields; 687 }, 688 689 /** 690 * Returns an array of the raw column objects returned from the server along with the query metadata 691 * 692 * @name getColumns 693 * @function 694 * @returns {Array} The columns associated with this store 695 * @memberOf LABKEY.ext4.data.Store# 696 * 697 */ 698 getColumns: function(){ 699 return this.proxy.reader.rawData.columnModel; 700 }, 701 702 /** 703 * Returns a field metadata object of the specified field 704 * 705 * @name findFieldMetadata 706 * @function 707 * @param {String} fieldName The name of the field 708 * @returns {Object} Metatdata for this field 709 * @memberOf LABKEY.ext4.data.Store# 710 * 711 */ 712 findFieldMetadata : function(fieldName){ 713 var fields = this.getFields(); 714 if (!fields) 715 return null; 716 717 return fields.get(fieldName); 718 }, 719 720 exportData : function(format) { 721 format = format || "excel"; 722 if (this.sql) 723 { 724 LABKEY.Query.exportSql({ 725 schemaName: this.schemaName, 726 sql: this.sql, 727 format: format, 728 containerPath: this.containerPath, 729 containerFilter: this.containerFilter 730 }); 731 } 732 else 733 { 734 var config = this.getExportConfig(format); 735 window.location = config.url; 736 } 737 }, 738 739 getExportConfig : function(format) { 740 741 format = format || "excel"; 742 743 var params = { 744 schemaName: this.schemaName, 745 "query.queryName": this.queryName, 746 "query.containerFilterName": this.containerFilter 747 }; 748 749 if (this.columns) { 750 params["query.columns"] = Ext4.isArray(this.columns) ? this.columns.join(',') : this.columns; 751 } 752 753 // These are filters that are custom created (aka not from a defined view). 754 LABKEY.Filter.appendFilterParams(params, this.filterArray); 755 756 if (this.sortInfo) { 757 params["query.sort"] = ("DESC" === this.sortInfo.direction ? "-" : "") + this.sortInfo.field; 758 } 759 760 var config = { 761 action: ("tsv" === format) ? "exportRowsTsv" : "exportRowsExcel", 762 params: params 763 }; 764 765 config.url = LABKEY.ActionURL.buildURL("query", config.action, this.containerPath, config.params); 766 767 return config; 768 }, 769 770 //Ext3 compatability?? 771 commitChanges: function(){ 772 this.sync(); 773 }, 774 775 //private 776 getKeyField: function(){ 777 return this.model.prototype.idProperty; 778 }, 779 780 //private, experimental 781 getQueryConfig: function(){ 782 return { 783 containerPath: this.containerPath, 784 schemaName: this.schemaName, 785 queryName: this.queryName, 786 viewName: this.viewName, 787 queryTitle: this.queryTitle, 788 sql: this.sql, 789 columns: this.columns, 790 filterArray: this.filterArray, 791 sort: this.initialConfig.sort, 792 maxRows: this.maxRows, 793 containerFilter: this.containerFilter 794 } 795 } 796 797 });