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