1 /** 2 * @fileOverview 3 * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>) 4 * @license Copyright (c) 2008-2018 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 EditorGridPanel using the supplied configuration. 24 * @class <p><font color="red">DEPRECATED</font> - Consider using 25 * <a href="http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a> instead.</p> 26 * <p>LabKey extension to the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a>, 27 * which can provide editable grid views of data in the LabKey server. If the current user has appropriate permissions, 28 * the user may edit data, save changes, insert new rows, or delete rows.</p> 29 * <p>If you use any of the LabKey APIs that extend Ext APIs, you must either make your code open source or 30 * <a href="https://www.labkey.org/Documentation/wiki-page.view?name=extDevelopment">purchase an Ext license</a>.</p> 31 * <p>Additional Documentation: 32 * <ul> 33 * <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=javascriptTutorial">Tutorial: Create Applications with the JavaScript API</a></li> 34 * </ul> 35 * </p> 36 * @constructor 37 * @augments Ext.grid.EditorGridPanel 38 * @param config Configuration properties. This may contain any of the configuration properties supported 39 * by the <a href="http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.EditorGridPanel">Ext.grid.EditorGridPanel</a>, 40 * plus those listed here. 41 * @param {boolean} [config.lookups] Set to false if you do not want lookup foreign keys to be resolved to the 42 * lookup values, and do not want dropdown lookup value pickers (default is true). 43 * @param {integer} [config.pageSize] Defines how many rows are shown at a time in the grid (default is 20). 44 * If the EditorGridPanel is getting its data from Ext.Store, pageSize will override the value of maxRows on Ext.Store. 45 * @param {boolean} [config.editable] Set to true if you want the user to be able to edit, insert, or delete rows (default is false). 46 * @param {boolean} [config.autoSave] Set to false if you do not want changes automatically saved when the user leaves the row (default is true). 47 * @param {boolean} [config.enableFilters] True to enable user-filtering of columns (default is false) 48 * @param {string} [config.loadingCaption] The string to display in a cell when loading the lookup values (default is "[loading...]"). 49 * @param {string} [config.lookupNullCaption] The string to display for a null value in a lookup column (default is "[none]"). 50 * @param {boolean} [config.showExportButton] Set to false to hide the Export button in the toolbar. True by default. 51 * @example Basic Example: <pre name="code" class="xml"> 52 <script type="text/javascript"> 53 var _grid; 54 55 //Use the Ext.onReady() function to define what code should 56 //be executed once the page is fully loaded. 57 //you must use this if you supply a renderTo config property 58 Ext.onReady(function(){ 59 _grid = new LABKEY.ext.EditorGridPanel({ 60 store: new LABKEY.ext.Store({ 61 schemaName: 'lists', 62 queryName: 'People' 63 }), 64 renderTo: 'grid', 65 width: 800, 66 autoHeight: true, 67 title: 'Example', 68 editable: true 69 }); 70 }); 71 </script> 72 <div id='grid'/> </pre> 73 * @example Advanced Example: 74 * 75 This snippet shows how to link a column in an EditorGridPanel to a details/update 76 page. It adds a custom column renderer to the grid column model by hooking 77 the 'columnmodelcustomize' event. Since the column is a lookup, it is helpful to 78 chain the base renderer so that it does the lookup magic for you. <pre name="code" class="xml"> 79 <script type="text/javascript"> 80 var _materialTemplate; 81 var _baseFormulationRenderer; 82 83 function formulationRenderer(data, cellMetaData, record, rowIndex, colIndex, store) 84 { 85 return _materialTemplate.apply(record.data) + _baseFormulationRenderer(data, 86 cellMetaData, record, rowIndex, colIndex, store) + '</a>'; 87 } 88 89 function customizeColumnModel(colModel, index) 90 { 91 if (colModel != undefined) 92 { 93 var col = index['Formulation']; 94 var url = LABKEY.ActionURL.buildURL("experiment", "showMaterial"); 95 96 _materialTemplate = new Ext.XTemplate('<a href="' + url + 97 '?rowId={Formulation}">').compile(); 98 _baseFormulationRenderer = col.renderer; 99 col.renderer = formulationRenderer; 100 } 101 } 102 103 Ext.onReady(function(){ 104 _grid = new LABKEY.ext.EditorGridPanel({ 105 store: new LABKEY.ext.Store({ 106 schemaName: 'lists', 107 queryName: 'FormulationExpMap' 108 }), 109 renderTo: 'gridDiv', 110 width: 600, 111 autoHeight: true, 112 title: 'Formulations to Experiments', 113 editable: true 114 }); 115 _grid.on("columnmodelcustomize", customizeColumnModel); 116 }); 117 </script></pre> 118 */ 119 LABKEY.ext.EditorGridPanel = Ext.extend(Ext.grid.EditorGridPanel, { 120 initComponent : function() { 121 122 Ext.QuickTips.init(); 123 Ext.apply(Ext.QuickTips.getQuickTip(), { 124 dismissDelay: 15000 125 }); 126 127 //set config defaults 128 Ext.applyIf(this, { 129 lookups: true, 130 pageSize: 20, 131 editable: false, 132 enableFilters: false, 133 autoSave: true, 134 loadingCaption: "[loading...]", 135 lookupNullCaption: "[none]", 136 viewConfig: {forceFit: true}, 137 id: Ext.id(undefined, "labkey-ext-grid"), 138 loadMask: true, 139 colModel: new Ext.grid.ColumnModel([]), 140 selModel: new Ext.grid.CheckboxSelectionModel({moveEditorOnEnter: false}), 141 showExportButton: true 142 }); 143 this.setupDefaultPanelConfig(); 144 145 LABKEY.ext.EditorGridPanel.superclass.initComponent.apply(this, arguments); 146 147 /** 148 * @memberOf LABKEY.ext.EditorGridPanel# 149 * @name columnmodelcustomize 150 * @event 151 * @description Use this event to customize the default column model config generated by the server. 152 * For details on the column model config, see the Ext API documentation for Ext.grid.ColumnModel 153 * (http://www.extjs.com/deploy/dev/docs/?class=Ext.grid.ColumnModel) 154 * @param {Ext.grid.ColumnModel} columnModel The default ColumnModel config generated by the server. 155 * @param {Object} index An index map where the key is column name and the value is the entry in the column 156 * model config for that column. Since the column model config is a simple array of objects, this index helps 157 * you get to the specific columns you need to modify without doing a sequential scan. 158 */ 159 /** 160 * @memberOf LABKEY.ext.EditorGridPanel# 161 * @name beforedelete 162 * @event 163 * @description Use this event to cancel the deletion of a row in the grid. If you return false 164 * from this event, the row will not be deleted 165 * @param {array} records An array of Ext.data.Record objects that the user wishes to delete 166 */ 167 168 this.addEvents("beforedelete, columnmodelcustomize"); 169 170 //subscribe to superclass events 171 this.on("beforeedit", this.onBeforeEdit, this); 172 this.on("render", this.onGridRender, this); 173 174 //subscribe to store events and start loading it 175 if(this.store) 176 { 177 this.store.on("loadexception", this.onStoreLoadException, this); 178 this.store.on("load", this.onStoreLoad, this); 179 this.store.on("beforecommit", this.onStoreBeforeCommit, this); 180 this.store.on("commitcomplete", this.onStoreCommitComplete, this); 181 this.store.on("commitexception", this.onStoreCommitException, this); 182 this.store.load({ params : { 183 start: 0, 184 limit: this.pageSize, 185 'X-LABKEY-CSRF': LABKEY.CSRF 186 }}); 187 } 188 }, 189 190 /** 191 * Returns the LABKEY.ext.Store object used to hold the 192 * lookup values for the specified column name. If the column 193 * name is not a lookup column, this method will return null. 194 * @name getLookupStore 195 * @function 196 * @memberOf LABKEY.ext.EditorGridPanel# 197 * @param {String} columnName The column name. 198 * @return {LABKEY.ext.Store} The lookup store for the given column name, or null 199 * if no lookup store exists for that column. 200 */ 201 getLookupStore : function(columnName) { 202 return this.store.getLookupStore(columnName); 203 }, 204 205 /** 206 * Saves all pending changes to the database. Note that if 207 * the required fields for a given record does not have values, 208 * that record will not be saved and will remain dirty until 209 * values are supplied for all required fields. 210 * @name saveChanges 211 * @function 212 * @memberOf LABKEY.ext.EditorGridPanel# 213 */ 214 saveChanges : function() { 215 this.stopEditing(); 216 this.getStore().commitChanges(); 217 }, 218 219 /*-- Private Methods --*/ 220 221 setupDefaultPanelConfig : function() { 222 if(!this.tbar) 223 { 224 this.tbar = [{ 225 text: 'Refresh', 226 tooltip: 'Click to refresh the table', 227 id: 'refresh-button', 228 handler: this.onRefresh, 229 scope: this 230 }]; 231 232 if(this.editable && LABKEY.user && LABKEY.user.canUpdate && !this.autoSave) 233 { 234 this.tbar.push("-"); 235 this.tbar.push({ 236 text: 'Save Changes', 237 tooltip: 'Click to save all changes to the database', 238 id: 'save-button', 239 handler: this.saveChanges, 240 scope: this 241 }); 242 } 243 244 if(this.editable &&LABKEY.user && LABKEY.user.canInsert) 245 { 246 this.tbar.push("-"); 247 this.tbar.push({ 248 text: 'Add Record', 249 tooltip: 'Click to add a row', 250 id: 'add-record-button', 251 handler: this.onAddRecord, 252 scope: this 253 }); 254 } 255 if(this.editable &&LABKEY.user && LABKEY.user.canDelete) 256 { 257 this.tbar.push("-"); 258 this.tbar.push({ 259 text: 'Delete Selected', 260 tooltip: 'Click to delete selected row(s)', 261 id: 'delete-records-button', 262 handler: this.onDeleteRecords, 263 scope: this 264 }); 265 } 266 267 if (this.showExportButton) 268 { 269 this.tbar.push("-"); 270 this.tbar.push({ 271 text: 'Export', 272 tooltip: 'Click to Export the data to Excel', 273 id: 'export-records-button', 274 handler: function(){ 275 if (this.store) 276 this.store.exportData("excel"); 277 }, 278 scope: this 279 }); 280 } 281 } 282 283 if(!this.bbar) 284 { 285 this.bbar = new Ext.PagingToolbar({ 286 pageSize: this.pageSize, //default is 20 287 store: this.store, 288 displayInfo: true, 289 emptyMsg: "No data to display" //display message when no records found 290 }); 291 } 292 293 if(!this.keys) 294 { 295 this.keys = [ 296 { 297 key: Ext.EventObject.ENTER, 298 handler: this.onEnter, 299 scope: this 300 }, 301 { 302 key: 45, //insert 303 handler: this.onAddRecord, 304 scope: this 305 }, 306 { 307 key: Ext.EventObject.ESC, 308 handler: this.onEsc, 309 scope: this 310 }, 311 { 312 key: Ext.EventObject.TAB, 313 handler: this.onTab, 314 scope: this 315 }, 316 { 317 key: Ext.EventObject.F2, 318 handler: this.onF2, 319 scope: this 320 } 321 ]; 322 } 323 }, 324 325 onStoreLoad : function(store, records, options) { 326 this.store.un("load", this.onStoreLoad, this); 327 328 this.populateMetaMap(); 329 this.setupColumnModel(); 330 }, 331 332 onStoreLoadException : function(proxy, options, response, error) { 333 var msg = error; 334 if (!msg && response.responseText) 335 { 336 try 337 { 338 var json = Ext.util.JSON.decode(response.responseText); 339 if (json) 340 msg = json.exception; 341 } 342 catch (err) 343 {} 344 } 345 if (!msg) 346 msg = "Unable to load data from the server!"; 347 348 Ext.Msg.alert("Error", msg); 349 }, 350 351 onStoreBeforeCommit : function(records, rows) { 352 //disable the refresh button so that it will animate 353 var pagingBar = this.getBottomToolbar(); 354 if(pagingBar && pagingBar.loading) 355 pagingBar.loading.disable(); 356 if(!this.savingMessage) 357 this.savingMessage = pagingBar.addText("Saving Changes..."); 358 else 359 this.savingMessage.setVisible(true); 360 }, 361 362 onStoreCommitComplete : function() { 363 var pagingBar = this.getBottomToolbar(); 364 if(pagingBar && pagingBar.loading) 365 pagingBar.loading.enable(); 366 if(this.savingMessage) 367 this.savingMessage.setVisible(false); 368 }, 369 370 onStoreCommitException : function(message) { 371 var pagingBar = this.getBottomToolbar(); 372 if(pagingBar && pagingBar.loading) 373 pagingBar.loading.enable(); 374 if(this.savingMessage) 375 this.savingMessage.setVisible(false); 376 }, 377 378 onGridRender : function() { 379 //add the extContainer class to the view's hmenu 380 //NOTE: there is no public API to get to hmenu and colMenu 381 //so this might break in future versions of Ext. If you get 382 //a JavaScript error on these lines, look at the API docs for 383 //a method or property that returns the sort and column hide/show 384 //menus shown from the column headers 385 // this.getView().hmenu.getEl().addClass("extContainer"); 386 // this.getView().colMenu.getEl().addClass("extContainer"); 387 388 //set up filtering 389 if (this.enableFilters) 390 this.initFilterMenu(); 391 392 }, 393 394 populateMetaMap : function() { 395 //the metaMap is a map from field name to meta data about the field 396 //the meta data contains the following properties: 397 // id, totalProperty, root, fields[] 398 // fields[] is an array of objects with the following properties 399 // name, type, lookup 400 // lookup is a nested object with the following properties 401 // schema, table, keyColumn, displayColumn 402 this.metaMap = {}; 403 var fields = this.store.reader.jsonData.metaData.fields; 404 for(var idx = 0; idx < fields.length; ++idx) 405 { 406 var field = fields[idx]; 407 this.metaMap[field.name] = field; 408 } 409 }, 410 411 setupColumnModel : function() { 412 413 //set the columns property to the columnModel returned in the jsonData 414 this.columns = this.store.reader.jsonData.columnModel; 415 416 //set the renderers and editors for the various columns 417 //build a column model index as we run the columns for the 418 //customize event 419 var colModelIndex = {}; 420 var col; 421 var meta; 422 for(var idx = 0; idx < this.columns.length; ++idx) 423 { 424 col = this.columns[idx]; 425 meta = this.metaMap[col.dataIndex]; 426 427 //this.editable can override col.editable 428 col.editable = this.editable && col.editable; 429 430 //if column type is boolean, substitute an Ext.grid.CheckColumn 431 if(meta.type == "boolean" || meta.type == "bool") 432 { 433 col = this.columns[idx] = new Ext.grid.CheckColumn(col); 434 if(col.editable) 435 col.init(this); 436 col.editable = false; //check columns apply edits immediately, so we don't want to go into edit mode 437 } 438 439 if(meta.hidden || meta.isHidden) 440 col.hidden = true; 441 442 if(col.editable && !col.editor) 443 col.editor = this.getDefaultEditor(col, meta); 444 if(!col.renderer) 445 col.renderer = this.getDefaultRenderer(col, meta); 446 447 //remember the first editable column (used during add record) 448 if(!this.firstEditableColumn && col.editable) 449 this.firstEditableColumn = idx; 450 451 //HTML-encode the column header 452 if(col.header) 453 col.header = Ext.util.Format.htmlEncode(col.header); 454 455 colModelIndex[col.dataIndex] = col; 456 } 457 458 //if a sel model has been set, and if it needs to be added as a column, 459 //add it to the front of the list. 460 //CheckBoxSelectionModel needs to be added to the column model for 461 //the check boxes to show up. 462 //(not sure why its constructor doesn't do this automatically). 463 if(this.getSelectionModel() && this.getSelectionModel().renderer) 464 this.columns = [this.getSelectionModel()].concat(this.columns); 465 466 //register for the rowdeselect event if the selmodel supports events 467 //and if autoSave is on 468 if(this.getSelectionModel().on && this.autoSave) 469 this.getSelectionModel().on("rowselect", this.onRowSelect, this); 470 471 //add custom renderers for multiline/long-text columns 472 this.setLongTextRenderers(); 473 474 //fire the "columnmodelcustomize" event to allow clients 475 //to modify our default configuration of the column model 476 this.fireEvent("columnmodelcustomize", this.columns, colModelIndex); 477 478 //reset the column model 479 this.reconfigure(this.store, new Ext.grid.ColumnModel(this.columns)); 480 }, 481 482 getDefaultRenderer : function(col, meta) { 483 if(meta.lookup && this.lookups && col.editable) //no need to use a lookup renderer if column is not editable 484 return this.getLookupRenderer(col, meta); 485 486 return function(data, cellMetaData, record, rowIndex, colIndex, store) 487 { 488 if(record.json && record.json[meta.name] && record.json[meta.name].mvValue) 489 { 490 var mvValue = record.json[meta.name].mvValue; 491 //get corresponding message from qcInfo section of JSON and set up a qtip 492 if(store.reader.jsonData.qcInfo && store.reader.jsonData.qcInfo[mvValue]) 493 { 494 cellMetaData.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(store.reader.jsonData.qcInfo[mvValue]) + "\""; 495 cellMetaData.css = "labkey-mv"; 496 } 497 return mvValue; 498 } 499 500 if(record.json && record.json[meta.name] && record.json[meta.name].displayValue) 501 return record.json[meta.name].displayValue; 502 503 if(null == data || undefined == data || data.toString().length == 0) 504 return data; 505 506 //format data into a string 507 var displayValue; 508 switch (meta.type) 509 { 510 case "date": 511 var date = new Date(data); 512 if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0) 513 displayValue = date.format("Y-m-d"); 514 else 515 displayValue = date.format("Y-m-d H:i:s"); 516 break; 517 case "string": 518 case "boolean": 519 case "int": 520 case "float": 521 default: 522 displayValue = data.toString(); 523 } 524 525 //if meta.file is true, add an <img> for the file icon 526 if(meta.file) 527 { 528 displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(data) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue; 529 //since the icons are 16x16, cut the default padding down to just 1px 530 cellMetaData.attr = "style=\"padding: 1px 1px 1px 1px\""; 531 } 532 533 //wrap in <a> if url is present in the record's original JSON 534 if(col.showLink !== false && record.json && record.json[meta.name] && record.json[meta.name].url) 535 return "<a href=\"" + record.json[meta.name].url + "\">" + displayValue + "</a>"; 536 else 537 return displayValue; 538 }; 539 }, 540 541 getLookupRenderer : function(col, meta) { 542 var lookupStore = this.store.getLookupStore(meta.name, !col.required); 543 lookupStore.on("loadexception", this.onLookupStoreError, this); 544 lookupStore.on("load", this.onLookupStoreLoad, this); 545 546 return function(data, cellMetaData, record, rowIndex, colIndex, store) 547 { 548 if(record.json && record.json[meta.name] && record.json[meta.name].displayValue) 549 return record.json[meta.name].displayValue; 550 551 if(null == data || undefined == data || data.toString().length == 0) 552 return data; 553 554 if(lookupStore.loadError) 555 return "ERROR: " + lookupStore.loadError.message; 556 557 if(0 === lookupStore.getCount() && !lookupStore.isLoading) 558 { 559 lookupStore.load(); 560 return "loading..."; 561 } 562 563 var lookupRecord = lookupStore.getById(data); 564 if (lookupRecord) 565 return lookupRecord.data[meta.lookup.displayColumn]; 566 else if (data) 567 return "[" + data + "]"; 568 else 569 return this.lookupNullCaption || "[none]"; 570 }; 571 }, 572 573 onLookupStoreLoad : function(store, records, options) { 574 if(this.view && !this.activeEditor) 575 this.view.refresh(); 576 }, 577 578 onLookupStoreError : function(proxy, type, action, options, response) 579 { 580 var message = ""; 581 if (type == 'response') 582 { 583 var ctype = response.getResponseHeader("Content-Type"); 584 if(ctype.indexOf("application/json") >= 0) 585 { 586 var errorJson = Ext.util.JSON.decode(response.responseText); 587 if(errorJson && errorJson.exception) 588 message = errorJson.exception; 589 } 590 } 591 else 592 { 593 if (response && response.exception) 594 { 595 message = response.exception; 596 } 597 } 598 Ext.Msg.alert("Load Error", "Error loading lookup data"); 599 600 if(this.view) 601 this.view.refresh(); 602 }, 603 604 getDefaultEditor : function(col, meta) { 605 var editor; 606 607 //if this column is a lookup, return the lookup editor 608 if(meta.lookup && this.lookups) 609 return this.getLookupEditor(col, meta); 610 611 switch(meta.type) 612 { 613 case "int": 614 editor = new Ext.form.NumberField({ 615 allowDecimals : false 616 }); 617 break; 618 case "float": 619 editor = new Ext.form.NumberField({ 620 allowDecimals : true 621 }); 622 break; 623 case "date": 624 editor = new Ext.form.DateField({ 625 format : "Y-m-d", 626 altFormats: "Y-m-d" + 627 'n/j/y g:i:s a|n/j/Y g:i:s a|n/j/y G:i:s|n/j/Y G:i:s|' + 628 'n-j-y g:i:s a|n-j-Y g:i:s a|n-j-y G:i:s|n-j-Y G:i:s|' + 629 'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' + 630 'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' + 631 'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' + 632 'n/j/y|n/j/Y|' + 633 'n-j-y|n-j-Y|' + 634 'j-M-y|j-M-Y|' + 635 'Y-n-d H:i:s|Y-n-d|' + 636 'j M Y H:i:s' // 10 Sep 2009 01:24:12 637 }); 638 //HACK: the DateMenu is created by the DateField 639 //and there's no config on DateField that lets you specify 640 //a CSS class to add to the DateMenu. If we create it now, 641 //their code will just use the one we create. 642 //See DateField.js in the Ext source 643 editor.menu = new Ext.menu.DateMenu({cls: 'extContainer'}); 644 break; 645 case "boolean": 646 editor = new Ext.form.Checkbox(); 647 break; 648 case "string": 649 default: 650 editor = new Ext.form.TextField(); 651 break; 652 } 653 654 if (editor) 655 editor.allowBlank = !col.required; 656 657 return editor; 658 }, 659 660 getLookupEditor : function(col, meta) { 661 var store = this.store.getLookupStore(meta.name, !col.required); 662 return new Ext.form.ComboBox({ 663 store: store, 664 allowBlank: !col.required, 665 typeAhead: false, 666 triggerAction: 'all', 667 editable: false, 668 displayField: meta.lookup.displayColumn, 669 valueField: meta.lookup.keyColumn, 670 tpl : '<tpl for="."><div class="x-combo-list-item">{[values["' + meta.lookup.displayColumn + '"]]}</div></tpl>', //FIX: 5860 671 listClass: 'labkey-grid-editor' 672 }); 673 }, 674 675 setLongTextRenderers : function() { 676 var col; 677 for(var idx = 0; idx < this.columns.length; ++idx) 678 { 679 col = this.columns[idx]; 680 if(col.multiline || (undefined === col.multiline && col.scale > 255 && this.metaMap[col.dataIndex].type === "string")) 681 { 682 col.renderer = function(data, metadata, record, rowIndex, colIndex, store) 683 { 684 //set quick-tip attributes and let Ext QuickTips do the work 685 metadata.attr = "ext:qtip=\"" + Ext.util.Format.htmlEncode(data) + "\""; 686 return data; 687 }; 688 689 if(col.editable) 690 col.editor = new LABKEY.ext.LongTextField({ 691 columnName: col.dataIndex 692 }); 693 } 694 } 695 }, 696 697 onRefresh : function() { 698 this.getStore().reload(); 699 }, 700 701 onAddRecord : function() { 702 if(!this.store || !this.store.addRecord) 703 return; 704 705 this.stopEditing(); 706 this.store.addRecord({}, 0); //add a blank record in the first position 707 this.getSelectionModel().selectFirstRow(); 708 this.startEditing(0, this.firstEditableColumn); 709 }, 710 711 onDeleteRecords : function() { 712 var records = this.getSelectionModel().getSelections(); 713 if (records && records.length) 714 { 715 if(this.fireEvent("beforedelete", {records: records})) 716 { 717 Ext.Msg.show({ 718 title: "Confirm Delete", 719 msg: records.length > 1 720 ? "Are you sure you want to delete the " 721 + records.length + " selected records? This cannot be undone." 722 : "Are you sure you want to delete the selected record? This cannot be undone.", 723 icon: Ext.MessageBox.QUESTION, 724 buttons: {ok: "Delete", cancel: "Cancel"}, 725 scope: this, 726 fn: function(buttonId) { 727 if(buttonId == "ok") 728 this.store.deleteRecords(records); 729 } 730 }); 731 } 732 } 733 }, 734 735 onRowSelect : function(selmodel, rowIndex) { 736 if(this.autoSave) 737 this.saveChanges(); 738 }, 739 740 onBeforeEdit : function(evt) { 741 if(this.getStore().isUpdateInProgress(evt.record)) 742 return false; 743 744 if(!this.getSelectionModel().isSelected(evt.row)) 745 this.getSelectionModel().selectRow(evt.row); 746 747 var editor = this.getColumnModel().getCellEditor(evt.column, evt.row); 748 var displayValue = (evt.record.json && evt.record.json[evt.field]) ? evt.record.json[evt.field].displayValue : undefined; 749 750 //set the value not found text to be the display value if there is one 751 if(editor && editor.field && editor.field.displayField && displayValue) 752 editor.field.valueNotFoundText = displayValue; 753 754 //reset combo mode to local if the lookup store is already populated 755 if(editor && editor.field && editor.field.displayField && editor.field.store && editor.field.store.getCount() > 0) 756 editor.field.mode = "local"; 757 }, 758 759 onEnter : function() { 760 this.stopEditing(); 761 762 //move selection down to the next row, or commit if on last row 763 var selmodel = this.getSelectionModel(); 764 if(selmodel.hasNext()) 765 selmodel.selectNext(); 766 else if(this.autoSave) 767 this.saveChanges(); 768 }, 769 770 onEsc : function() { 771 //if the currently selected record is dirty, 772 //reject the edits 773 var record = this.getSelectionModel().getSelected(); 774 if(record && record.dirty) 775 { 776 if(record.isNew) 777 this.getStore().remove(record); 778 else 779 record.reject(); 780 } 781 }, 782 783 onTab : function() { 784 if(this.autoSave) 785 this.saveChanges(); 786 }, 787 788 onF2 : function() { 789 var record = this.getSelectionModel().getSelected(); 790 if(record) 791 { 792 var index = this.getStore().findBy(function(recordComp, id){return id == record.id;}); 793 if(index >= 0 && undefined !== this.firstEditableColumn) 794 this.startEditing(index, this.firstEditableColumn); 795 } 796 797 }, 798 799 initFilterMenu : function() 800 { 801 var filterItem = new Ext.menu.Item({text:"Filter...", scope:this, handler:function() {this.handleFilter();}}); 802 var hmenu = this.getView().hmenu; 803 // hmenu.getEl().addClass("extContainer"); 804 hmenu.addItem(filterItem); 805 }, 806 807 handleFilter :function () 808 { 809 var view = this.getView(); 810 var col = view.cm.config[view.hdCtxIndex]; 811 812 this.showFilterWindow(col); 813 }, 814 815 showFilterWindow: function(col) 816 { 817 var colName = col.dataIndex; 818 var meta = this.getStore().findFieldMeta(colName); 819 var grid = this; //Stash for later use in callbacks. 820 821 var filterColName = meta.lookup ? colName + "/" + meta.lookup.displayColumn : colName; 822 var filterColType; 823 if (meta.lookup) 824 { 825 var lookupStore = this.store.getLookupStore(filterColName); 826 if (null != lookupStore) 827 { 828 meta = lookupStore.findFieldMeta(meta.lookup.displayColumn); 829 filterColType = meta ? meta.type : "string"; 830 } 831 else 832 filterColType = "string"; 833 } 834 else 835 filterColType = meta.type; 836 837 var colFilters = this.getColumnFilters(colName); 838 var dropDowns = [ 839 LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 1 ? colFilters[0].getFilterType().getURLSuffix() : null, true), 840 LABKEY.ext.EditorGridPanel.createFilterCombo(filterColType, colFilters.length >= 2 ? colFilters[1].getFilterType().getURLSuffix() : null)]; 841 var valueEditors = [ 842 new Ext.form.TextField({value:colFilters.length > 0 ? colFilters[0].getValue() : "",width:250}), 843 new Ext.form.TextField({value:colFilters.length > 1 ? colFilters[1].getValue() : "",width:250, hidden:colFilters.length < 2, hideMode:'visibility'})]; 844 845 dropDowns[0].valueEditor = valueEditors[0]; 846 dropDowns[1].valueEditor = valueEditors[1]; 847 848 function validateEntry(index) 849 { 850 var filterType = dropDowns[index].getFilterType(); 851 return filterType.validate(valueEditors[index].getValue(), filterColType, colName); 852 } 853 854 var win = new Ext.Window({ 855 title:"Show Rows Where " + colName, 856 width:400, 857 autoHeight:true, 858 modal:true, 859 items:[dropDowns[0], valueEditors[0], new Ext.form.Label({text:" and"}), 860 dropDowns[1], valueEditors[1]], 861 //layout:'column', 862 buttons:[ 863 { 864 text:"OK", 865 handler:function() { 866 var filters = []; 867 var value; 868 value = validateEntry(0); 869 if (!value) 870 return; 871 872 var filterType = dropDowns[0].getFilterType(); 873 filters.push(LABKEY.Filter.create(filterColName, value, filterType)); 874 filterType = dropDowns[1].getFilterType(); 875 if (filterType && filterType.getURLSuffix().length > 0) 876 { 877 value = validateEntry(1); 878 if (!value) 879 return; 880 filters.push(LABKEY.Filter.create(filterColName, value, filterType)); 881 } 882 grid.setColumnFilters(colName, filters); 883 win.close(); 884 } 885 }, 886 { 887 text:"Cancel", 888 handler:function() {win.close();} 889 }, 890 { 891 text:"Clear Filter", 892 handler:function() {grid.setColumnFilters(colName, []); win.close();} 893 }, 894 { 895 text:"Clear All Filters", 896 handler:function() {grid.getStore().setUserFilters([]); grid.getStore().load({params:{start:0, limit:grid.pageSize}}); win.close()} 897 } 898 ] 899 }); 900 win.show(); 901 //Focus doesn't work right away (who knows why?) so defer it... 902 function f() {valueEditors[0].focus();}; 903 f.defer(100); 904 }, 905 906 getColumnFilters: function(colName) 907 { 908 var colFilters = []; 909 Ext.each(this.getStore().getUserFilters(), function(filter) { 910 if (filter.getColumnName() == colName) 911 colFilters.push(filter); 912 }); 913 return colFilters; 914 }, 915 916 setColumnFilters: function(colName, filters) 917 { 918 var newFilters = []; 919 Ext.each(this.getStore().getUserFilters(), function(filter) { 920 if (filter.getColumnName() != colName) 921 newFilters.push(filter); 922 }); 923 if (filters) 924 Ext.each(filters, function(filter) {newFilters.push(filter);}); 925 926 this.getStore().setUserFilters(newFilters); 927 this.getStore().load({params:{start:0, limit:this.pageSize}}); 928 } 929 }); 930 931 LABKEY.ext.EditorGridPanel.createFilterCombo = function (type, filterOp, first) 932 { 933 var ft = LABKEY.Filter.Types; 934 var defaultFilterTypes = { 935 "int":ft.EQUAL, "string":ft.STARTS_WITH, "boolean":ft.EQUAL, "float":ft.GTE, "date":ft.DATE_EQUAL 936 }; 937 938 //Option lists for drop-downs. Filled in on-demand based on filter type 939 var dropDownOptions = []; 940 Ext.each(LABKEY.Filter.getFilterTypesForType(type), function (filterType) { 941 dropDownOptions.push([filterType.getURLSuffix(), filterType.getDisplayText()]); 942 }); 943 944 //Do the ext magic for the options. Gets easier in ext 2.2 945 var options = (!first) ? [['', 'no other filter']].concat(dropDownOptions) : dropDownOptions; 946 var store = new Ext.data.SimpleStore({'id': 0, fields: ['value', 'text'], data: options }); 947 var combo = new Ext.form.ComboBox({ 948 store:store, 949 forceSelection:true, 950 valueField:'value', 951 displayField:'text', 952 mode:'local', 953 allowBlank:false, 954 triggerAction:'all', 955 value:filterOp ? filterOp : ((!first) ? '' : defaultFilterTypes[type].getURLSuffix()) 956 }); 957 combo.on("select", function(combo, record, itemNo) { 958 var filter = this.getFilterType(); 959 if (this.valueEditor) 960 this.valueEditor.setVisible(filter != null && filter.isDataValueRequired()); 961 }); 962 963 combo.getFilterType = function () { 964 return LABKEY.Filter.getFilterTypeForURLSuffix(this.getValue()); 965 }; 966 967 return combo; 968 }; 969 970 971 // Check column plugin 972 Ext.grid.CheckColumn = function(config){ 973 Ext.apply(this, config); 974 if(!this.id){ 975 this.id = Ext.id(); 976 } 977 this.renderer = this.renderer.createDelegate(this); 978 }; 979 980 Ext.grid.CheckColumn.prototype ={ 981 init : function(grid){ 982 this.grid = grid; 983 if(grid.getView() && grid.getView().mainBody) 984 { 985 grid.getView().mainBody.on('mousedown', this.onMouseDown, this); 986 } 987 else 988 { 989 this.grid.on('render', function(){ 990 var view = this.grid.getView(); 991 view.mainBody.on('mousedown', this.onMouseDown, this); 992 }, this); 993 } 994 }, 995 996 onMouseDown : function(e, t){ 997 if(t.className && t.className.indexOf('x-grid3-cc-'+this.id) != -1){ 998 e.stopEvent(); 999 var index = this.grid.getView().findRowIndex(t); 1000 var record = this.grid.store.getAt(index); 1001 this.grid.getSelectionModel().selectRow(index); 1002 record.set(this.dataIndex, !record.data[this.dataIndex]); 1003 } 1004 }, 1005 1006 renderer : function(v, p, record){ 1007 p.css += ' x-grid3-check-col-td'; 1008 return '<div class="x-grid3-check-col'+(v?'-on':'')+' x-grid3-cc-'+this.id+'"> </div>'; 1009 } 1010 }; 1011