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