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 LABKEY.FilterDialog = Ext.extend(Ext.Window, { 7 8 autoHeight: true, 9 10 bbarCfg : { 11 bodyStyle : 'border-top: 1px solid black;' 12 }, 13 14 cls: 'labkey-filter-dialog', 15 16 closeAction: 'destroy', 17 18 defaults: { 19 border: false, 20 msgTarget: 'under' 21 }, 22 23 itemId: 'filterWindow', 24 25 modal: true, 26 27 resizable: false, 28 29 // 24846 30 width: Ext.isGecko ? 425 : 410, 31 32 allowFacet : undefined, 33 34 cacheFacetResults: true, 35 36 initComponent : function() { 37 38 if (!this['dataRegionName']) { 39 console.error('dataRegionName is required for a LABKEY.FilterDialog'); 40 return; 41 } 42 43 this.column = this.column || this.boundColumn; // backwards compat 44 if (!this.configureColumn(this.column)) { 45 return; 46 } 47 48 Ext.apply(this, { 49 title: this.title || "Show Rows Where " + this.column.caption + "...", 50 51 carryfilter : true, // whether filter state should try to be carried between views (e.g. when changing tabs) 52 53 // buttons 54 buttons: this.configureButtons(), 55 56 // hook key events 57 keys:[{ 58 key: Ext.EventObject.ENTER, 59 handler: this.onApply, 60 scope: this 61 },{ 62 key: Ext.EventObject.ESC, 63 handler: this.closeDialog, 64 scope: this 65 }], 66 67 // listeners 68 listeners: { 69 destroy: function() { 70 if (this.focusTask) { 71 Ext.TaskMgr.stop(this.focusTask); 72 } 73 }, 74 resize : function(panel) { panel.syncShadow(); }, 75 scope : this 76 } 77 }); 78 79 this.items = [this.getContainer()]; 80 81 LABKEY.FilterDialog.superclass.initComponent.call(this); 82 }, 83 84 allowFaceting : function() { 85 if (Ext.isDefined(this.allowFacet)) 86 return this.allowFacet; 87 88 var dr = this.getDataRegion(); 89 if (!this.isQueryDataRegion(dr)) { 90 this.allowFacet = false; 91 return this.allowFacet; 92 } 93 94 this.allowFacet = false; 95 switch (this.column.facetingBehaviorType) { 96 97 case 'ALWAYS_ON': 98 this.allowFacet = true; 99 break; 100 case 'ALWAYS_OFF': 101 this.allowFacet = false; 102 break; 103 case 'AUTOMATIC': 104 // auto rules are if the column is a lookup or dimension 105 // OR if it is of type : (boolean, int, date, text), multiline excluded 106 if (this.column.lookup || this.column.dimension) 107 this.allowFacet = true; 108 else if (this.jsonType == 'boolean' || this.jsonType == 'int' || 109 (this.jsonType == 'string' && this.column.inputType != 'textarea')) 110 this.allowFacet = true; 111 break; 112 } 113 114 return this.allowFacet; 115 }, 116 117 // Returns an Array of button configurations based on supported operations on this column 118 configureButtons : function() { 119 var buttons = [ 120 {text: 'OK', handler: this.onApply, scope: this}, 121 {text: 'Cancel', handler: this.closeDialog, scope: this} 122 ]; 123 124 if (this.getDataRegion()) { 125 buttons.push({text: 'Clear Filter', handler: this.clearFilter, scope: this}); 126 buttons.push({text: 'Clear All Filters', handler: this.clearAllFilters, scope: this}); 127 } 128 129 return buttons; 130 }, 131 132 // Returns true if the initialization was a success 133 configureColumn : function(column) { 134 if (!column) { 135 console.error('A column is required for LABKEY.FilterDialog'); 136 return false; 137 } 138 139 Ext.apply(this, { 140 // DEPRECATED: Either invoked from GWT, which will handle the commit itself. 141 // Or invoked as part of a regular filter dialog on a grid 142 changeFilterCallback: this.confirmCallback, 143 144 fieldCaption: column.caption, 145 fieldKey: column.lookup && column.displayField ? column.displayField : column.fieldKey, // terrible 146 jsonType: (column.displayFieldJsonType ? column.displayFieldJsonType : column.jsonType) || 'string' 147 }); 148 149 return true; 150 }, 151 152 onApply : function() { 153 if (this.apply()) 154 this.closeDialog(); 155 }, 156 157 // Validates and applies the current filter(s) to the DataRegion 158 apply : function() { 159 var view = this.getContainer().getActiveTab(); 160 var isValid = true; 161 162 if (!view.getForm().isValid()) 163 isValid = false; 164 165 if (isValid) { 166 isValid = view.checkValid(); 167 } 168 169 if (isValid) { 170 171 var dr = this.getDataRegion(), 172 filters = view.getFilters(); 173 174 if (Ext.isFunction(this.changeFilterCallback)) { 175 176 var filterParams = '', sep = ''; 177 for (var f=0; f < filters.length; f++) { 178 filterParams += sep + encodeURIComponent(filters[f].getURLParameterName(this.dataRegionName)) + '=' + encodeURIComponent(filters[f].getURLParameterValue()); 179 sep = '&'; 180 } 181 this.changeFilterCallback.call(this, null, null, filterParams); 182 } 183 else { 184 if (filters.length > 0) { 185 // add the current filter(s) 186 if (view.supportsMultipleFilters) { 187 dr.replaceFilters(filters, this.column); 188 } 189 else 190 dr.replaceFilter(filters[0]); 191 } 192 else { 193 this.clearFilter(); 194 } 195 } 196 } 197 198 return isValid; 199 }, 200 201 clearFilter : function() { 202 var dr = this.getDataRegion(); 203 if (!dr) { return; } 204 Ext.StoreMgr.clear(); 205 dr.clearFilter(this.fieldKey); 206 this.closeDialog(); 207 }, 208 209 clearAllFilters : function() { 210 var dr = this.getDataRegion(); 211 if (!dr) { return; } 212 dr.clearAllFilters(); 213 this.closeDialog(); 214 }, 215 216 closeDialog : function() { 217 this.close(); 218 }, 219 220 getDataRegion : function() { 221 return LABKEY.DataRegions[this.dataRegionName]; 222 }, 223 224 isQueryDataRegion : function(dr) { 225 return dr && dr.schemaName && dr.queryName; 226 }, 227 228 // Returns a class instance of a class that extends Ext.Container. 229 // This container will hold all the views registered to this FilterDialog instance. 230 // For caching purposes assign to this.viewcontainer 231 getContainer : function() { 232 233 if (!this.viewcontainer) { 234 235 var views = this.getViews(); 236 var type = 'TabPanel'; 237 238 if (views.length == 1) { 239 views[0].title = false; 240 type = 'Panel'; 241 } 242 243 var config = { 244 defaults: this.defaults, 245 deferredRender: false, 246 monitorValid: true, 247 248 // sizing and styling 249 autoHeight: true, 250 bodyStyle: 'margin: 0 5px;', 251 border: true, 252 items: views 253 }; 254 255 if (type == 'TabPanel') { 256 config.listeners = { 257 beforetabchange : function(tp, newTab, oldTab) { 258 if (this.carryfilter && newTab && oldTab && oldTab.isChanged()) { 259 newTab.setFilters(oldTab.getFilters()); 260 } 261 }, 262 tabchange : function() { 263 this.syncShadow(); 264 this.viewcontainer.getActiveTab().doLayout(); // required when facets return while on another tab 265 }, 266 scope : this 267 }; 268 } 269 270 if (views.length > 1) { 271 config.activeTab = (this.allowFaceting() ? 1 : 0); 272 } 273 else { 274 views[0].title = false; 275 } 276 277 this.viewcontainer = new Ext[type](config); 278 279 if (!Ext.isFunction(this.viewcontainer.getActiveTab)) { 280 var me = this; 281 this.viewcontainer.getActiveTab = function() { 282 return me.viewcontainer.items.items[0]; 283 }; 284 // views attempt to hook the 'activate' event but some panel types do not fire 285 // force fire on the first view 286 this.viewcontainer.items.items[0].on('afterlayout', function(p) { 287 p.fireEvent('activate', p); 288 }, this, {single: true}); 289 } 290 } 291 292 return this.viewcontainer; 293 }, 294 295 _getFilters : function() { 296 var filters = []; 297 298 var dr = this.getDataRegion(); 299 if (dr) { 300 Ext.each(dr.getUserFilterArray(), function(ff) { 301 if (this.column.lookup && this.column.displayField && ff.getColumnName().toLowerCase() === this.column.displayField.toLowerCase()) { 302 filters.push(ff); 303 } 304 else if (this.column.fieldKey && ff.getColumnName().toLowerCase() === this.column.fieldKey.toLowerCase()) { 305 filters.push(ff); 306 } 307 }, this); 308 } 309 else if (this.queryString) { // deprecated 310 filters = LABKEY.Filter.getFiltersFromUrl(this.queryString, this.dataRegionName); 311 } 312 313 return filters; 314 }, 315 316 // Override to return your own filter views 317 getViews : function() { 318 319 var filters = this._getFilters(), views = []; 320 321 // default view 322 views.push({ 323 xtype: 'filter-view-default', 324 column: this.column, 325 fieldKey: this.fieldKey, // should not have to hand this in bc the column should supply correctly 326 dataRegionName: this.dataRegionName, 327 jsonType : this.jsonType, 328 filters: filters 329 }); 330 331 // facet view 332 if (this.allowFaceting()) { 333 views.push({ 334 xtype: 'filter-view-faceted', 335 column: this.column, 336 fieldKey: this.fieldKey, // should not have to hand this in bc the column should supply correctly 337 dataRegionName: this.dataRegionName, 338 jsonType : this.jsonType, 339 filters: filters, 340 cacheResults: this.cacheFacetResults, 341 listeners: { 342 invalidfilter : function() { 343 this.carryfilter = false; 344 this.getContainer().setActiveTab(0); 345 this.getContainer().getActiveTab().doLayout(); 346 this.carryfilter = true; 347 }, 348 scope: this 349 }, 350 scope: this 351 }) 352 } 353 354 return views; 355 } 356 }); 357 358 LABKEY.FilterDialog.ViewPanel = Ext.extend(Ext.form.FormPanel, { 359 360 supportsMultipleFilters: false, 361 362 filters : [], 363 364 changed : false, 365 366 initComponent : function() { 367 if (!this['dataRegionName']) { 368 console.error('dataRegionName is requied for a LABKEY.FilterDialog.ViewPanel'); 369 return; 370 } 371 LABKEY.FilterDialog.ViewPanel.superclass.initComponent.call(this); 372 }, 373 374 // Override to provide own view validation 375 checkValid : function() { 376 return true; 377 }, 378 379 getDataRegion : function() { 380 return LABKEY.DataRegions[this.dataRegionName]; 381 }, 382 383 getFilters : function() { 384 console.error('All classes which extend LABKEY.FilterDialog.ViewPanel must implement getFilters()'); 385 }, 386 387 setFilters : function(filterArray) { 388 console.error('All classes which extend LABKEY.FilterDialog.ViewPanel must implement setFilters(filterArray)'); 389 }, 390 391 getXtype : function() { 392 switch (this.jsonType) { 393 case "date": 394 return "datefield"; 395 case "int": 396 case "float": 397 return "textfield"; 398 case "boolean": 399 return 'labkey-booleantextfield'; 400 default: 401 return "textfield"; 402 } 403 }, 404 405 // Returns true if a view has been altered since the last time it was activated 406 isChanged : function() { 407 return this.changed; 408 } 409 }); 410 411 Ext.ns('LABKEY.FilterDialog.View'); 412 413 LABKEY.FilterDialog.View.Default = Ext.extend(LABKEY.FilterDialog.ViewPanel, { 414 415 supportsMultipleFilters: true, 416 417 itemDefaults: { 418 border: false, 419 msgTarget: 'under' 420 }, 421 422 initComponent : function() { 423 424 Ext.apply(this, { 425 autoHeight: true, 426 title: this.title === false ? false : 'Choose Filters', 427 bodyStyle: 'padding: 5px;', 428 bubbleEvents: ['add', 'remove', 'clientvalidation'], 429 defaults: { border: false }, 430 items: this.generateFilterDisplays(2) 431 }); 432 433 this.combos = []; 434 this.inputs = []; 435 436 LABKEY.FilterDialog.View.Default.superclass.initComponent.call(this); 437 438 this.on('activate', this.onViewReady, this, {single: true}); 439 }, 440 441 onViewReady : function() { 442 if (this.filters.length == 0) { 443 for (var c=0; c < this.combos.length; c++) { 444 // Update the input enabled/disabled status by using the 'select' event listener on the combobox. 445 // However, ComboBox doesn't fire 'select' event when changed programatically so we fire it manually. 446 this.combos[c].reset(); 447 this.combos[c].fireEvent('select', this.combos[c], null); 448 this.inputs[c].reset(); 449 } 450 } 451 else { 452 for (var f=0; f < this.filters.length; f++) { 453 if (f < this.combos.length) { 454 var filter = this.filters[f]; 455 var combo = this.combos[f]; 456 457 // Update the input enabled/disabled status by using the 'select' event listener on the combobox. 458 // However, ComboBox doesn't fire 'select' event when changed programatically so we fire it manually. 459 var store = combo.getStore(); 460 if (store) { 461 var rec = store.getAt(store.find('value', filter.getFilterType().getURLSuffix())); 462 if (rec) { 463 combo.setValue(filter.getFilterType().getURLSuffix()); 464 combo.fireEvent('select', combo, rec); 465 } 466 } 467 468 this.inputs[f].setValue(filter.getURLParameterValue()); 469 } 470 } 471 } 472 473 //Issue 24550: always select the first filter field, and also select text if present 474 if (this.inputs[0]) { 475 this.inputs[0].focus(true, 100, this.inputs[0]); 476 } 477 478 this.changed = false; 479 }, 480 481 checkValid : function() { 482 var combos = this.combos; 483 var inputs = this.inputs, input, value, f; 484 485 var isValid = true; 486 487 Ext.each(combos, function(c, i) { 488 if (!c.isValid()) { 489 isValid = false; 490 } 491 else { 492 input = inputs[i]; 493 value = input.getValue(); 494 495 f = LABKEY.Filter.getFilterTypeForURLSuffix(c.getValue()); 496 497 if (!f) { 498 alert('filter not found: ' + c.getValue()); 499 return; 500 } 501 502 if (f.isDataValueRequired() && Ext.isEmpty(value)) { 503 input.markInvalid('You must enter a value'); 504 isValid = false; 505 } 506 } 507 }); 508 509 return isValid; 510 }, 511 512 inputFieldValidator : function(input, combo) { 513 514 var store = combo.getStore(); 515 if (store) { 516 var rec = store.getAt(store.find('value', combo.getValue())); 517 var filter = LABKEY.Filter.getFilterTypeForURLSuffix(combo.getValue()); 518 519 if (rec) { 520 if (filter.isMultiValued()) 521 return this.validateMultiValueInput(input.getValue(), filter.getMultiValueSeparator(), filter.getMultiValueMinOccurs(), filter.getMultiValueMaxOccurs()); 522 return this.validateInputField(input.getValue()); 523 } 524 } 525 return true; 526 }, 527 528 generateFilterDisplays : function(quantity) { 529 var idx = this.nextIndex(), items = [], i=0; 530 531 for(; i < quantity; i++) { 532 items.push({ 533 xtype: 'panel', 534 layout: 'form', 535 itemId: 'filterPair' + idx, 536 border: false, 537 defaults: this.itemDefaults, 538 items: [this.getComboConfig(idx), this.getInputConfig(idx)], 539 scope: this 540 }); 541 idx++; 542 } 543 return items; 544 }, 545 546 getComboConfig : function(idx) { 547 548 var val = idx === 0 ? LABKEY.Filter.getDefaultFilterForType(this.jsonType).getURLSuffix() : ''; 549 return { 550 xtype: 'combo', 551 itemId: 'filterComboBox' + idx, 552 filterIndex: idx, 553 name: 'filterType_'+(idx + 1), //for compatibility with tests... 554 listWidth: (this.jsonType == 'date' || this.jsonType == 'boolean') ? null : 380, 555 emptyText: idx === 0 ? 'Choose a filter:' : 'No other filter', 556 autoSelect: false, 557 width: 250, 558 minListWidth: 250, 559 triggerAction: 'all', 560 fieldLabel: (idx === 0 ?'Filter Type' : 'and'), 561 store: this.getSelectionStore(idx), 562 displayField: 'text', 563 valueField: 'value', 564 typeAhead: 'false', 565 forceSelection: true, 566 mode: 'local', 567 clearFilterOnReset: false, 568 editable: false, 569 value: val, 570 originalValue: val, 571 listeners : { 572 render : function(combo) { 573 this.combos.push(combo); 574 // Update the associated inputField's enabled/disabled state on initial render 575 this.enableInputField(combo); 576 }, 577 select : function (combo) { 578 this.changed = true; 579 this.enableInputField(combo); 580 }, 581 scope: this 582 }, 583 scope: this 584 }; 585 }, 586 587 enableInputField : function (combo) { 588 589 var idx = combo.filterIndex; 590 var inputField = this.find('itemId', 'inputField'+idx)[0]; 591 592 var filter = LABKEY.Filter.getFilterTypeForURLSuffix(combo.getValue()); 593 var selectedValue = filter ? filter.getURLSuffix() : ''; 594 595 var combos = this.combos; 596 var inputFields = this.inputs; 597 598 if (filter && !filter.isDataValueRequired()) { 599 //Disable the field and allow it to be blank for values 'isblank' and 'isnonblank'. 600 inputField.disable(); 601 inputField.setValue(); 602 } 603 else { 604 inputField.enable(); 605 inputField.validate(); 606 inputField.focus('', 50) 607 } 608 609 //if the value is null, this indicates no filter chosen. if it lacks an operator (ie. isBlank) 610 //in either case, this means we should disable all other filters 611 if(selectedValue == '' || !filter.isDataValueRequired()){ 612 //Disable all subsequent combos 613 Ext.each(combos, function(combo, idx){ 614 //we enable the next combo in the series 615 if(combo.filterIndex == this.filterIndex + 1){ 616 combo.setValue(); 617 inputFields[idx].setValue(); 618 inputFields[idx].enable(); 619 inputFields[idx].validate(); 620 } 621 else if (combo.filterIndex > this.filterIndex){ 622 combo.setValue(); 623 inputFields[idx].disable(); 624 } 625 626 }, this); 627 } 628 else{ 629 //enable the other filterComboBoxes. 630 Ext.each(combos, function(combo, i) { combo.enable(); }, this); 631 632 if (combos.length) { 633 combos[0].focus('', 50); 634 } 635 } 636 }, 637 638 getFilters : function() { 639 640 var inputs = this.inputs; 641 var combos = this.combos; 642 var value, type, filters = []; 643 644 Ext.each(combos, function(c, i) { 645 if (!inputs[i].disabled || (c.getRawValue() != 'No Other Filter')) { 646 value = inputs[i].getValue(); 647 type = LABKEY.Filter.getFilterTypeForURLSuffix(c.getValue()); 648 649 if (!type) { 650 alert('Filter not found for suffix: ' + c.getValue()); 651 } 652 653 filters.push(LABKEY.Filter.create(this.fieldKey, value, type)); 654 } 655 }, this); 656 657 return filters; 658 }, 659 660 getInputConfig : function(idx) { 661 var me = this; 662 return { 663 xtype : this.getXtype(), 664 itemId : 'inputField' + idx, 665 filterIndex : idx, 666 id : 'value_'+(idx + 1), //for compatibility with tests... 667 width : 250, 668 blankText : 'You must enter a value.', 669 validateOnBlur: true, 670 value : null, 671 altFormats: (this.jsonType == "date" ? LABKEY.Utils.getDateAltFormats() : undefined), 672 validator : function(value) { 673 674 // support for filtering '∞' 675 if (me.jsonType == 'float' && value.indexOf('∞') > -1) { 676 value = value.replace('∞', 'Infinity'); 677 this.setRawValue(value); // does not fire validation 678 } 679 680 var combos = me.combos; 681 if (!combos.length) { 682 return; 683 } 684 685 return me.inputFieldValidator(this, combos[idx]); 686 }, 687 listeners: { 688 disable : function(field){ 689 //Call validate after disable so any pre-existing validation errors go away. 690 if(field.rendered) { 691 field.validate(); 692 } 693 }, 694 focus : function(f) { 695 if (this.focusTask) { 696 Ext.TaskMgr.stop(this.focusTask); 697 } 698 }, 699 render : function(input) { 700 me.inputs.push(input); 701 if (!me.focusReady) { 702 me.focusReady = true; 703 // create a task to set the input focus that will get started after layout is complete, 704 // the task will run for a max of 2000ms but will get stopped when the component receives focus 705 this.focusTask = {interval:150, run: function(){ 706 input.focus(null, 50); 707 Ext.TaskMgr.stop(this.focusTask); 708 }, scope: this, duration: 2000}; 709 } 710 }, 711 change : function(input, newVal, oldVal) { 712 if (oldVal != newVal) { 713 this.changed = true; 714 } 715 }, 716 scope : this 717 }, 718 scope: this 719 }; 720 }, 721 722 getSelectionStore : function(storeNum) { 723 var fields = ['text', 'value', 724 {name: 'isMulti', type: Ext.data.Types.BOOL}, 725 {name: 'isOperatorOnly', type: Ext.data.Types.BOOL} 726 ]; 727 var store = new Ext.data.ArrayStore({ 728 fields: fields, 729 idIndex: 1 730 }); 731 var comboRecord = Ext.data.Record.create(fields); 732 733 var filters = LABKEY.Filter.getFilterTypesForType(this.jsonType, this.column.mvEnabled); 734 for (var i=0; i<filters.length; i++) 735 { 736 var filter = filters[i]; 737 store.add(new comboRecord({ 738 text: filter.getLongDisplayText(), 739 value: filter.getURLSuffix(), 740 isMulti: filter.isMultiValued(), 741 isOperatorOnly: filter.isDataValueRequired() 742 })); 743 } 744 745 if (storeNum > 0) { 746 store.removeAt(0); 747 store.insert(0, new comboRecord({text:'No Other Filter', value: ''})); 748 } 749 750 return store; 751 }, 752 753 setFilters : function(filterArray) { 754 this.filters = filterArray; 755 this.onViewReady(); 756 }, 757 758 nextIndex : function() { 759 return 0; 760 }, 761 762 validateMultiValueInput : function(inputValues, multiValueSeparator, minOccurs, maxOccurs) { 763 // Used when "Equals One Of.." or "Between" is selected. Calls validateInputField on each value entered. 764 var values = inputValues.split(multiValueSeparator); 765 var isValid = ""; 766 for(var i = 0; i < values.length; i++){ 767 isValid = this.validateInputField(values[i]); 768 if(isValid !== true){ 769 return isValid; 770 } 771 } 772 773 if (minOccurs !== undefined && minOccurs > 0) 774 { 775 if (values.length < minOccurs) 776 return "At least " + minOccurs + " '" + multiValueSeparator + "' separated values are required"; 777 } 778 779 if (maxOccurs !== undefined && maxOccurs > 0) 780 { 781 if (values.length > maxOccurs) 782 return "At most " + maxOccurs + " '" + multiValueSeparator + "' separated values are allowed"; 783 } 784 785 //If we make it out of the for loop we had no errors. 786 return true; 787 }, 788 789 // The fact that Ext3 ties validation to the editor is a little funny, 790 // but using this shifts the work to Ext 791 validateInputField : function(value) { 792 var map = { 793 'string': 'STRING', 794 'int': 'INT', 795 'float': 'FLOAT', 796 'date': 'DATE', 797 'boolean': 'BOOL' 798 }; 799 var type = map[this.jsonType]; 800 if (type) { 801 var field = new Ext.data.Field({ 802 type: Ext.data.Types[type], 803 allowDecimals : this.jsonType != "int", //will be ignored by anything besides numberfield 804 useNull: true 805 }); 806 807 var convertedVal = field.convert(value); 808 if (!Ext.isEmpty(value) && value != convertedVal) { 809 return "Invalid value: " + value; 810 } 811 } 812 else { 813 console.log('Unrecognized type: ' + this.jsonType); 814 } 815 816 return true; 817 } 818 }); 819 820 Ext.reg('filter-view-default', LABKEY.FilterDialog.View.Default); 821 822 LABKEY.FilterDialog.View.Faceted = Ext.extend(LABKEY.FilterDialog.ViewPanel, { 823 824 MAX_FILTER_CHOICES: 250, // This is the maximum number of filters that will be requested / shown 825 826 applyContextFilters: true, 827 828 /** 829 * Logically convert filters to try and optimize the query on the server. 830 * (e.g. using NOT IN when less than half the available values are checked) 831 */ 832 filterOptimization: true, 833 834 cacheResults: true, 835 836 emptyDisplayValue: '[Blank]', 837 838 gridID: Ext.id(), 839 840 initComponent : function() { 841 842 Ext.apply(this, { 843 title : 'Choose Values', 844 border : false, 845 height : 200, 846 bodyStyle: 'overflow-x: hidden; overflow-y: auto', 847 bubbleEvents: ['add', 'remove', 'clientvalidation'], 848 defaults : { 849 border : false 850 }, 851 markDisabled : true, 852 items: [{ 853 layout: 'hbox', 854 style: 'padding-bottom: 5px; overflow-x: hidden', 855 defaults: { 856 border: false 857 }, 858 items: [{ 859 xtype: 'label', 860 id: this.gridID + 'OverflowLabel', 861 hidden: true, 862 text: 'There are more than ' + this.MAX_FILTER_CHOICES + ' values. Showing a partial list.' 863 }] 864 }] 865 }); 866 867 LABKEY.FilterDialog.View.Faceted.superclass.initComponent.call(this); 868 869 this.on('render', this.onPanelRender, this, {single: true}); 870 }, 871 872 formatValue : function(val) { 873 if(this.column) { 874 if (this.column.extFormatFn) { 875 try { 876 this.column.extFormatFn = eval(this.column.extFormatFn); 877 } 878 catch (error) { 879 console.log('improper extFormatFn: ' + this.column.extFormatFn); 880 } 881 882 if (Ext.isFunction(this.column.extFormatFn)) { 883 val = this.column.extFormatFn(val); 884 } 885 } 886 else if (this.jsonType == 'int') { 887 val = parseInt(val); 888 } 889 } 890 return val; 891 }, 892 893 // copied from Ext 4 Ext.Array.difference 894 difference : function(arrayA, arrayB) { 895 var clone = arrayA.slice(), 896 ln = clone.length, 897 i, j, lnB; 898 899 for (i = 0,lnB = arrayB.length; i < lnB; i++) { 900 for (j = 0; j < ln; j++) { 901 if (clone[j] === arrayB[i]) { 902 clone.splice(j, 1); 903 j--; 904 ln--; 905 } 906 } 907 } 908 909 return clone; 910 }, 911 912 constructFilter : function(selected, unselected) { 913 var filter = null; 914 915 if (selected.length > 0) { 916 917 var columnName = this.fieldKey; 918 919 // one selection 920 if (selected.length == 1) { 921 if (selected[0].get('displayValue') == this.emptyDisplayValue) 922 filter = LABKEY.Filter.create(columnName, null, LABKEY.Filter.Types.ISBLANK); 923 else 924 filter = LABKEY.Filter.create(columnName, selected[0].get('value')); // default EQUAL 925 } 926 else if (this.filterOptimization && selected.length > unselected.length) { 927 // Do the negation 928 if (unselected.length == 1) { 929 var val = unselected[0].get('value'); 930 var type = (val === "" ? LABKEY.Filter.Types.NONBLANK : LABKEY.Filter.Types.NOT_EQUAL_OR_MISSING); 931 932 // 18716: Check if 'unselected' contains empty value 933 filter = LABKEY.Filter.create(columnName, val, type); 934 } 935 else 936 filter = LABKEY.Filter.create(columnName, this.selectedToValues(unselected), LABKEY.Filter.Types.NOT_IN); 937 } 938 else { 939 filter = LABKEY.Filter.create(columnName, this.selectedToValues(selected), LABKEY.Filter.Types.IN); 940 } 941 } 942 943 return filter; 944 }, 945 946 // get array of values from the selected store item array 947 selectedToValues : function(valueArray) { 948 return valueArray.map(function (i) { return i.get('value'); }); 949 }, 950 951 // Implement interface LABKEY.FilterDialog.ViewPanel 952 getFilters : function() { 953 var grid = Ext.getCmp(this.gridID); 954 var filters = []; 955 956 if (grid) { 957 var store = grid.store; 958 var count = store.getCount(); // TODO: Check if store loaded 959 var selected = grid.getSelectionModel().getSelections(); 960 961 if (count == 0 || selected.length == 0 || selected.length == count) { 962 filters = []; 963 } 964 else { 965 var unselected = this.filterOptimization ? this.difference(store.getRange(), selected) : []; 966 filters = [this.constructFilter(selected, unselected)]; 967 } 968 } 969 970 return filters; 971 }, 972 973 // Implement interface LABKEY.FilterDialog.ViewPanel 974 setFilters : function(filterArray) { 975 if (Ext.isArray(filterArray)) { 976 this.filters = filterArray; 977 this.onViewReady(); 978 } 979 }, 980 981 getGridConfig : function(idx) { 982 var sm = new Ext.grid.CheckboxSelectionModel({ 983 listeners: { 984 selectionchange: { 985 fn: function(sm) { 986 // NOTE: this will manually set the checked state of the header checkbox. it would be better 987 // to make this a real tri-state (ie. selecting some records is different then none), but since this is still Ext3 988 // and ext4 will be quite different it doesnt seem worth the effort right now 989 var selections = sm.getSelections(); 990 var headerCell = Ext.fly(sm.grid.getView().getHeaderCell(0)).first('div'); 991 if(selections.length == sm.grid.store.getCount()){ 992 headerCell.addClass('x-grid3-hd-checker-on'); 993 } 994 else { 995 headerCell.removeClass('x-grid3-hd-checker-on'); 996 } 997 998 999 }, 1000 buffer: 50 1001 } 1002 } 1003 }); 1004 1005 var me = this; 1006 1007 return { 1008 xtype: 'grid', 1009 id: this.gridID, 1010 border: true, 1011 bodyBorder: true, 1012 frame: false, 1013 autoHeight: true, 1014 itemId: 'inputField' + (idx || 0), 1015 filterIndex: idx || 0, 1016 msgTarget: 'title', 1017 store: this.getLookupStore(), 1018 headerClick: false, 1019 viewConfig: { 1020 headerTpl: new Ext.Template( 1021 '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">', 1022 '<thead>', 1023 '<tr class="x-grid3-row-table">{cells}</tr>', 1024 '</thead>', 1025 '</table>' 1026 ) 1027 }, 1028 sm: sm, 1029 cls: 'x-grid-noborder', 1030 columns: [ 1031 sm, 1032 new Ext.grid.TemplateColumn({ 1033 header: '<a href="javascript:void(0);">[All]</a>', 1034 dataIndex: 'value', 1035 menuDisabled: true, 1036 resizable: false, 1037 width: 340, 1038 tpl: new Ext.XTemplate('<tpl for=".">' + 1039 '<span class="labkey-link" title="{[Ext.util.Format.htmlEncode(values["displayValue"])]}">' + 1040 '{[Ext.util.Format.htmlEncode(values["displayValue"])]}' + 1041 '</span></tpl>') 1042 }) 1043 ], 1044 listeners: { 1045 afterrender : function(grid) { 1046 grid.getSelectionModel().on('selectionchange', function() { 1047 this.changed = true; 1048 }, this); 1049 1050 grid.on('viewready', function(g) { 1051 this.gridReady = true; 1052 this.onViewReady(); 1053 }, this, {single: true}); 1054 }, 1055 scope : this 1056 }, 1057 // extend toggle behavior to the header cell, not just the checkbox next to it 1058 onHeaderCellClick : function() { 1059 var sm = this.getSelectionModel(); 1060 var selected = sm.getSelections(); 1061 selected.length == this.store.getCount() ? this.selectNone() : this.selectAll(); 1062 }, 1063 getValue : function() { 1064 var vals = this.getValues(); 1065 if (vals.length == vals.max) { 1066 return []; 1067 } 1068 return vals.values; 1069 }, 1070 getValues : function() { 1071 var values = [], 1072 sels = this.getSelectionModel().getSelections(); 1073 1074 Ext.each(sels, function(rec){ 1075 values.push(rec.get('strValue')); 1076 }, this); 1077 1078 if(values.indexOf('') != -1 && values.length == 1) 1079 values.push(''); //account for null-only filtering 1080 1081 return { 1082 values : values.join(';'), 1083 length : values.length, 1084 max : this.getStore().getCount() 1085 }; 1086 }, 1087 setValue : function(values, negated) { 1088 if (!this.rendered) { 1089 this.on('render', function() { 1090 this.setValue(values, negated); 1091 }, this, {single: true}); 1092 } 1093 1094 if (!Ext.isArray(values)) { 1095 values = values.split(';'); 1096 } 1097 1098 if (this.store.isLoading) { 1099 // need to wait for the store to load to ensure records 1100 this.store.on('load', function() { 1101 this._checkAndLoadValues(values, negated); 1102 }, this, {single: true}); 1103 } 1104 else { 1105 this._checkAndLoadValues(values, negated); 1106 } 1107 }, 1108 _checkAndLoadValues : function(values, negated) { 1109 var records = [], 1110 recIdx, 1111 recordNotFound = false; 1112 1113 Ext.each(values, function(val) { 1114 recIdx = this.store.findBy(function(rec){ 1115 return rec.get('strValue') === val; 1116 }); 1117 1118 if (recIdx != -1) { 1119 records.push(recIdx); 1120 } 1121 else { 1122 // Issue 14710: if the record isnt found, we wont be able to select it, so should reject. 1123 // If it's null/empty, ignore silently 1124 if (!Ext.isEmpty(val)) { 1125 recordNotFound = true; 1126 return false; 1127 } 1128 } 1129 }, this); 1130 1131 if (negated) { 1132 var count = this.store.getCount(), found = false, negRecords = []; 1133 for (var i=0; i < count; i++) { 1134 found = false; 1135 for (var j=0; j < records.length; j++) { 1136 if (records[j] == i) 1137 found = true; 1138 } 1139 if (!found) { 1140 negRecords.push(i); 1141 } 1142 } 1143 records = negRecords; 1144 } 1145 1146 if (recordNotFound) { 1147 // cannot find any matching records 1148 if (me.column.facetingBehaviorType != 'ALWAYS_ON') 1149 me.fireEvent('invalidfilter'); 1150 return; 1151 } 1152 1153 this.getSelectionModel().selectRows(records); 1154 }, 1155 selectAll : function() { 1156 if (this.rendered) { 1157 var sm = this.getSelectionModel(); 1158 sm.selectAll.defer(10, sm); 1159 } 1160 else { 1161 this.on('render', this.selectAll, this, {single: true}); 1162 } 1163 }, 1164 selectNone : function() { 1165 if (this.rendered) { 1166 this.getSelectionModel().selectRows([]); 1167 } 1168 else { 1169 this.on('render', this.selectNone, this, {single: true}); 1170 } 1171 }, 1172 determineNegation: function(filter) { 1173 var suffix = filter.getFilterType().getURLSuffix(); 1174 var negated = suffix == 'neqornull' || suffix == 'notin'; 1175 1176 // negation of the null case is a bit different so check it as a special case. 1177 var value = filter.getURLParameterValue(); 1178 if (value == "" && suffix != 'isblank') { 1179 negated = true; 1180 } 1181 return negated; 1182 }, 1183 selectFilter : function(filter) { 1184 var negated = this.determineNegation(filter); 1185 1186 this.setValue(filter.getURLParameterValue(), negated); 1187 1188 if (!me.filterOptimization && negated) { 1189 me.fireEvent('invalidfilter'); 1190 } 1191 }, 1192 scope : this 1193 }; 1194 }, 1195 1196 onViewReady : function() { 1197 if (this.gridReady && this.storeReady) { 1198 var grid = Ext.getCmp(this.gridID); 1199 this.hideMask(); 1200 1201 if (grid) { 1202 1203 var numFilters = this.filters.length; 1204 var numFacets = grid.store.getCount(); 1205 1206 // apply current filter 1207 if (numFacets == 0) 1208 grid.selectNone(); 1209 else if (numFilters == 0) 1210 grid.selectAll(); 1211 else 1212 grid.selectFilter(this.filters[0]); 1213 1214 if (!grid.headerClick) { 1215 grid.headerClick = true; 1216 var div = Ext.fly(grid.getView().getHeaderCell(1)).first('div'); 1217 div.on('click', grid.onHeaderCellClick, grid); 1218 } 1219 1220 // Issue 39727 - show a message if we've capped the number of options shown 1221 Ext.getCmp(this.gridID + 'OverflowLabel').setVisible(this.overflow); 1222 1223 if (this.overflow) { 1224 this.fireEvent('invalidfilter'); 1225 } 1226 } 1227 } 1228 1229 this.changed = false; 1230 }, 1231 1232 getLookupStore : function() { 1233 var dr = this.getDataRegion(); 1234 var storeId = this.cacheResults ? [dr.schemaName, dr.queryName, this.fieldKey].join('||') : Ext.id(); 1235 1236 // cache 1237 var store = Ext.StoreMgr.get(storeId); 1238 if (store) { 1239 this.storeReady = true; // unsafe 1240 return store; 1241 } 1242 1243 store = new Ext.data.ArrayStore({ 1244 fields : ['value', 'strValue', 'displayValue'], 1245 storeId: storeId 1246 }); 1247 1248 var config = { 1249 schemaName: dr.schemaName, 1250 queryName: dr.queryName, 1251 dataRegionName: dr.name, 1252 viewName: dr.viewName, 1253 column: this.fieldKey, 1254 filterArray: dr.filters, 1255 containerPath: dr.container || dr.containerPath || LABKEY.container.path, 1256 containerFilter: dr.getContainerFilter(), 1257 parameters: dr.getParameters(), 1258 maxRows: this.MAX_FILTER_CHOICES+1, 1259 ignoreFilter: dr.ignoreFilter, 1260 success : function(d) { 1261 if (d && d.values) { 1262 var recs = [], v, i=0, hasBlank = false, isString, formattedValue; 1263 1264 // Issue 39727 - remember if we exceeded our cap so we can show a message 1265 this.overflow = d.values.length > this.MAX_FILTER_CHOICES; 1266 1267 for (; i < Math.min(d.values.length, this.MAX_FILTER_CHOICES); i++) { 1268 v = d.values[i]; 1269 formattedValue = this.formatValue(v); 1270 isString = Ext.isString(formattedValue); 1271 1272 if (formattedValue == null || (isString && formattedValue.length == 0) || (!isString && isNaN(formattedValue))) { 1273 hasBlank = true; 1274 } 1275 else if (Ext.isDefined(v)) { 1276 recs.push([v, v.toString(), v.toString()]); 1277 } 1278 } 1279 1280 if (hasBlank) 1281 recs.unshift(['', '', this.emptyDisplayValue]); 1282 1283 store.loadData(recs); 1284 store.isLoading = false; 1285 this.storeReady = true; 1286 this.onViewReady(); 1287 } 1288 }, 1289 scope: this 1290 }; 1291 1292 if (this.applyContextFilters) { 1293 var userFilters = dr.getUserFilterArray(); 1294 if (userFilters && userFilters.length > 0) { 1295 1296 var uf = []; 1297 1298 // Remove filters for the current column 1299 for (var i=0; i < userFilters.length; i++) { 1300 if (userFilters[i].getColumnName() != this.fieldKey) { 1301 uf.push(userFilters[i]); 1302 } 1303 } 1304 1305 config.filterArray = uf; 1306 } 1307 } 1308 1309 // Use Select Distinct 1310 LABKEY.Query.selectDistinctRows(config); 1311 1312 return Ext.StoreMgr.add(store); 1313 }, 1314 1315 onPanelRender : function(panel) { 1316 var toAdd = [{ 1317 xtype: 'panel', 1318 width: this.width - 40, //prevent horizontal scroll 1319 bodyStyle: 'padding-left: 5px;', 1320 items: [ this.getGridConfig(0) ], 1321 listeners : { 1322 afterrender : { 1323 fn: this.showMask, 1324 scope: this, 1325 single: true 1326 } 1327 } 1328 }]; 1329 panel.add(toAdd); 1330 }, 1331 1332 showMask : function() { 1333 if (!this.gridReady && this.getEl()) { 1334 this.getEl().mask('Loading...'); 1335 } 1336 }, 1337 1338 hideMask : function() { 1339 if (this.getEl()) { 1340 this.getEl().unmask(); 1341 } 1342 } 1343 }); 1344 1345 Ext.reg('filter-view-faceted', LABKEY.FilterDialog.View.Faceted); 1346 1347 Ext.ns('LABKEY.ext'); 1348 1349 LABKEY.ext.BooleanTextField = Ext.extend(Ext.form.TextField, { 1350 initComponent : function() { 1351 Ext.apply(this, { 1352 validator: function(val){ 1353 if(!val) 1354 return true; 1355 1356 return LABKEY.Utils.isBoolean(val) ? true : val + " is not a valid boolean. Try true/false; yes/no; on/off; or 1/0."; 1357 } 1358 }); 1359 LABKEY.ext.BooleanTextField.superclass.initComponent.call(this); 1360 } 1361 }); 1362 1363 Ext.reg('labkey-booleantextfield', LABKEY.ext.BooleanTextField); 1364 1365