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 (function() { 7 8 /** 9 * @name LABKEY.ext4.Util 10 * @class 11 * Ext4 utilities, contains functions to return an Ext config object to create an Ext field based on 12 * the supplied metadata. 13 */ 14 Ext4.ns('LABKEY.ext4.Util'); 15 16 // TODO: Get these off the global 'Date' object 17 Ext4.ns('Date.patterns'); 18 Ext4.applyIf(Date.patterns, { 19 ISO8601Long:"Y-m-d H:i:s", 20 ISO8601Short:"Y-m-d" 21 }); 22 23 var warned = {}; 24 25 var Util = LABKEY.ext4.Util; 26 27 var caseInsensitiveEquals = function(a, b) { 28 a = String(a); 29 b = String(b); 30 return a.toLowerCase() == b.toLowerCase(); 31 }; 32 33 /** 34 * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and 35 * resizes it so the Container fits inside the its parent container. 36 * @param extContainer - (Required) outer container which is the target to be resized 37 * @param options - a set of options 38 * @param options.skipWidth - true to skip updating width, default false 39 * @param options.skipHeight - true to skip updating height, default false 40 * @param options.paddingWidth - total width padding 41 * @param options.paddingHeight - total height padding 42 * @param options.offsetY - distance between bottom of page to bottom of component 43 * @param options.overrideMinWidth - true to set ext container's minWidth value to calculated width, default false 44 */ 45 // resizeToContainer -- defined in Ext4/ext-patches.js 46 47 Ext4.apply(Util, { 48 49 /** 50 * A map to convert between jsonType and Ext type 51 */ 52 EXT_TYPE_MAP: { 53 'string': 'STRING', 54 'int': 'INT', 55 'float': 'FLOAT', 56 'date': 'DATE', 57 'boolean': 'BOOL' 58 }, 59 /** 60 * @lends LABKEY.ext4.Util 61 */ 62 63 /** 64 * @private 65 * @param config 66 */ 67 buildQtip: function(config) { 68 var qtip = []; 69 //NOTE: returned in the 9.1 API format 70 if(config.record && config.record.raw && config.record.raw[config.meta.name] && config.record.raw[config.meta.name].mvValue){ 71 var mvValue = config.record.raw[config.meta.name].mvValue; 72 73 //get corresponding message from qcInfo section of JSON and set up a qtip 74 if(config.record.store && config.record.store.reader.rawData && config.record.store.reader.rawData.qcInfo && config.record.store.reader.rawData.qcInfo[mvValue]) 75 { 76 qtip.push(config.record.store.reader.rawData.qcInfo[mvValue]); 77 config.cellMetaData.css = "labkey-mv"; 78 } 79 qtip.push(mvValue); 80 } 81 82 if (!config.record.isValid()){ 83 var errors = config.record.validate().getByField(config.meta.name); 84 if (errors.length){ 85 Ext4.Array.forEach(errors, function(e){ 86 qtip.push(e.message); 87 }, this); 88 } 89 } 90 91 if (config.meta.buildQtip) { 92 config.meta.buildQtip({ 93 qtip: config.qtip, 94 value: config.value, 95 cellMetaData: config.cellMetaData, 96 meta: config.meta, 97 record: config.record 98 }); 99 } 100 101 if (qtip.length) { 102 qtip = Ext4.Array.unique(qtip); 103 config.cellMetaData.tdAttr = config.cellMetaData.tdAttr || ''; 104 config.cellMetaData.tdAttr += " data-qtip=\"" + Ext4.util.Format.htmlEncode(qtip.join('<br>')) + "\""; 105 } 106 }, 107 108 /** 109 * @private 110 * @param store 111 * @param fieldName 112 */ 113 findFieldMetadata: function(store, fieldName) { 114 var fields = store.model.prototype.fields; 115 if(!fields) 116 return null; 117 118 return fields.get(fieldName); 119 }, 120 121 /** 122 * @private 123 * @param fieldObj 124 */ 125 findJsonType: function(fieldObj) { 126 var type = fieldObj.type || fieldObj.typeName; 127 128 if (type === 'DateTime') 129 return 'date'; 130 else if (type === 'Double') 131 return 'float'; 132 else if (type === 'Integer' || type === 'int') 133 return 'int'; 134 return 'string'; 135 }, 136 137 /** 138 * @private 139 * @param store 140 * @param col 141 * @param config 142 * @param grid 143 */ 144 getColumnConfig: function(store, col, config, grid) { 145 col = col || {}; 146 col.dataIndex = col.dataIndex || col.name; 147 col.header = col.header || col.caption || col.label || col.name; 148 149 var meta = Util.findFieldMetadata(store, col.dataIndex); 150 if(!meta){ 151 return; 152 } 153 154 col.customized = true; 155 156 col.hidden = meta.hidden; 157 col.format = meta.extFormat; 158 159 //this.updatable can override col.editable 160 col.editable = config.editable && col.editable && meta.userEditable; 161 162 if(col.editable && !col.editor) 163 col.editor = Util.getGridEditorConfig(meta); 164 165 col.renderer = Util.getDefaultRenderer(col, meta, grid); 166 167 //HTML-encode the column header 168 col.text = Ext4.util.Format.htmlEncode(meta.label || meta.name || col.header); 169 170 if(meta.ignoreColWidths) 171 delete col.width; 172 173 //allow override of defaults 174 if(meta.columnConfig) 175 Ext4.Object.merge(col, meta.columnConfig); 176 if(config && config[col.dataIndex]) 177 Ext4.Object.merge(col, config[col.dataIndex]); 178 179 return col; 180 }, 181 182 /** 183 * @private 184 * @param store 185 * @param grid 186 * @param config 187 */ 188 getColumnsConfig: function(store, grid, config) { 189 config = config || {}; 190 191 var fields = store.model.getFields(); 192 var columns = store.getColumns(); 193 var cols = []; 194 195 Ext4.each(fields, function(field) { 196 var col; 197 198 if (field.shownInGrid === false) 199 return; 200 201 for (var i=0;i<columns.length;i++){ 202 var c = columns[i]; 203 if (c.dataIndex == field.dataIndex) { 204 col = c; 205 break; 206 } 207 } 208 209 if (!col) 210 col = {dataIndex: field.dataIndex}; 211 212 //NOTE: In Ext4.1 if your store does not provide a key field, Ext will create a new column called 'id' 213 //this is somewhat of a problem, since it is difficult to differentiate this as automatically generated 214 var cfg = Util.getColumnConfig(store, col, config, grid); 215 if (cfg) 216 cols.push(cfg); 217 }, this); 218 219 return cols; 220 }, 221 222 /** 223 * @private 224 * @param displayValue 225 * @param value 226 * @param col 227 * @param meta 228 * @param record 229 */ 230 getColumnUrl: function(displayValue, value, col, meta, record) { 231 //wrap in <a> if url is present in the record's original JSON 232 var url; 233 if (meta.buildUrl) { 234 url = meta.buildUrl({ 235 displayValue: displayValue, 236 value: value, 237 col: col, 238 meta: meta, 239 record: record 240 }); 241 } 242 else if (record.raw && record.raw[meta.name] && record.raw[meta.name].url) { 243 url = record.raw[meta.name].url; 244 } 245 return Ext4.util.Format.htmlEncode(url); 246 }, 247 248 /** 249 * This is designed to be called through either .getFormEditorConfig or .getFormEditor. 250 * Uses the given meta-data to generate a field config object. 251 * 252 * This function accepts a collection of config parameters to be easily adapted to 253 * various different metadata formats. 254 * 255 * Note: you can provide any Ext config options using the editorConfig or formEditorConfig objects 256 * These config options can also be used to pass arbitrary config options used by your specific Ext component 257 * 258 * @param {string} [config.type] e.g. 'string','int','boolean','float', or 'date'. for consistency this will be translated into the property jsonType 259 * @param {object} [config.editable] 260 * @param {object} [config.required] 261 * @param {string} [config.label] used to generate fieldLabel 262 * @param {string} [config.name] used to generate fieldLabel (if header is null) 263 * @param {string} [config.caption] used to generate fieldLabel (if label is null) 264 * @param {integer} [config.cols] if input is a textarea, sets the width (style:width is better) 265 * @param {integer} [config.rows] if input is a textarea, sets the height (style:height is better) 266 * @param {string} [config.lookup.schemaName] the schema used for the lookup. schemaName also supported 267 * @param {string} [config.lookup.queryName] the query used for the lookup. queryName also supported 268 * @param {Array} [config.lookup.columns] The columns used by the lookup store. If not set, the <code>[keyColumn, displayColumn]</code> will be used. 269 * @param {string} [config.lookup.keyColumn] 270 * @param {string} [config.lookup.displayColumn] 271 * @param {string} [config.lookup.sort] The sort used by the lookup store. 272 * @param {boolean} [config.lookups] use lookups=false to prevent creating default combobox for lookup columns 273 * @param {object} [config.editorConfig] is a standard Ext config object (although it can contain any properties) that will be merged with the computed field config 274 * e.g. editorConfig:{width:120, tpl:new Ext.Template(...), arbitraryOtherProperty: 'this will be applied to the editor'} 275 * this will be merged will all form or grid editors 276 * @param {object} [config.formEditorConfig] Similar to editorConfig; however, it will only be merged when getFormEditor() or getFormEditorConfig() are called. 277 * The intention is to provide a mechanism so the same metadata object can be used to generate editors in both a form or a grid (or other contexts). 278 * @param {object} [config.gridEditorConfig] similar to formEditorConfig; however, it will only be merged when getGridEditor() or getGridEditorConfig() are called. 279 * @param {object} [config.columnConfig] similar to formEditorConfig; however, it will only be merged when getColumnConfig() is getColumnsConfig() called. 280 * @param {object} [config.lookup.store] advanced! Pass in your own custom store for a lookup field 281 * @param {boolean} [config.lazyCreateStore] If false, the store will be created immediately. If true, the store will be created when the component is created. (default true) 282 * @param {boolean} [config.createIfDoesNotExist] If true, this field will be created in the store, even if it does not otherwise exist on the server. Can be used to force custom fields to appear in a grid or form or to pass additional information to the server at time of import 283 * @param {function} [config.buildQtip] This function will be used to generate the qTip for the field when it appears in a grid instead of the default function. It will be passed a single object as an argument. This object has the following properties: qtip, data, cellMetaData, meta, record, store. Qtip is an array which will be merged to form the contents of the tooltip. Your code should modify the array to alter the tooltip. For example: 284 * buildQtip: function(config){ 285 * qtip.push('I have a tooltip!'); 286 * qtip.push('This is my value: ' + config.value); 287 * } 288 * @param {function} [config.buildDisplayString] This function will be used to generate the display string for the field when it appears in a grid instead of the default function. It will be passed the same argument as buildQtip() 289 * @param {function} [config.buildUrl] This function will be used to generate the URL encapsulating the field 290 * @param {string} [config.urlTarget] If the value is rendered in a LABKEY.ext4.EditorGridPanel (or any other component using this pathway), and it contains a URL, this will be used as the target of <a> tag. For example, use _blank for a new window. 291 * @param {boolean} [config.setValueOnLoad] If true, the store will attempt to set a value for this field on load. This is determined by the defaultValue or getInitialValue function, if either is defined 292 * @param {function} [config.getInitialValue] When a new record is added to this store, this function will be called on that field. If setValueOnLoad is true, this will also occur on load. It will be passed the record and metadata. The advantage of using a function over defaultValue is that more complex and dynamic initial values can be created. For example: 293 * //sets the value to the current date 294 * getInitialValue(val, rec, meta){ 295 * return val || new Date() 296 * } 297 * @param {boolean} [config.wordWrap] If true, when displayed in an Ext grid the contents of the cell will use word wrapping, as opposed to being forced to a single line 298 * 299 * Note: the follow Ext params are automatically defined based on the specified Labkey metadata property: 300 * dataIndex -> name 301 * editable -> userEditable && readOnly 302 * header -> caption 303 * xtype -> set within getDefaultEditorConfig() based on jsonType, unless otherwise provided 304 */ 305 getDefaultEditorConfig: function(meta) { 306 var field = { 307 //added 'caption' for assay support 308 fieldLabel : Ext4.util.Format.htmlEncode(meta.label || meta.caption || meta.caption || meta.header || meta.name), 309 originalConfig : meta, 310 //we assume the store's translateMeta() will handle this 311 allowBlank : (meta.allowBlank === true) || (meta.required !==true), 312 //disabled: meta.editable===false, 313 name : meta.name, 314 dataIndex : meta.dataIndex || meta.name, 315 value : meta.value || meta.defaultValue, 316 width : meta.width, 317 height : meta.height, 318 msgTarget : 'qtip', 319 validateOnChange : true 320 }; 321 322 var helpPopup = meta.helpPopup || (function() { 323 var array = []; 324 325 if (meta.friendlyType) 326 if (!(meta.lookup && meta.lookup['public'] !== false && meta.lookups !== false)) 327 array.push(meta.friendlyType); 328 329 if (meta.description) 330 array.push(Ext4.util.Format.htmlEncode(meta.description)); 331 332 if (!field.allowBlank) 333 array.push("This field is required."); 334 335 return array; 336 }()); 337 338 if (Ext4.isArray(helpPopup)) 339 helpPopup = helpPopup.join('<br>'); 340 field.helpPopup = helpPopup; 341 342 if (meta.hidden) { 343 field.xtype = 'hidden'; 344 field.hidden = true; 345 } 346 else if (meta.editable === false) { 347 field.xtype = 'displayfield'; 348 } 349 else if (meta.lookup && meta.lookup['public'] !== false && meta.lookups !== false && meta.facetingBehaviorType != 'ALWAYS_OFF') { 350 var l = meta.lookup; 351 352 //test whether the store has been created. create if necessary 353 if (Ext4.isObject(meta.store) && meta.store.events) { 354 field.store = meta.store; 355 } 356 else { 357 field.store = Util.getLookupStore(meta); 358 } 359 360 Ext4.apply(field, { 361 // the purpose of this is to allow other editors like multiselect, checkboxGroup, etc. 362 xtype : (meta.xtype || 'labkey-combo'), 363 forceSelection : true, 364 typeAhead : true, 365 queryMode : 'local', 366 displayField : l.displayColumn, 367 valueField : l.keyColumn, 368 //NOTE: supported for non-combo components 369 initialValue : field.value, 370 showValueInList : meta.showValueInList, 371 nullCaption : meta.nullCaption, 372 373 // explicit usages can override this to display HTML in their combo list items, 374 // but the default should be to HTML encode the display value 375 listConfig: { 376 getInnerTpl: function(displayField) { 377 return '{[LABKEY.Utils.encodeHtml(values.' + displayField + ')]}'; 378 } 379 } 380 }); 381 } 382 else { 383 switch (meta.jsonType) { 384 case "boolean": 385 field.xtype = meta.xtype || 'checkbox'; 386 if (field.value === true){ 387 field.checked = true; 388 } 389 break; 390 case "int": 391 field.xtype = meta.xtype || 'numberfield'; 392 field.allowDecimals = false; 393 break; 394 case "float": 395 field.xtype = meta.xtype || 'numberfield'; 396 field.allowDecimals = true; 397 break; 398 case "date": 399 field.xtype = meta.xtype || 'datefield'; 400 field.format = meta.extFormat || Date.patterns.ISO8601Long; 401 field.altFormats = LABKEY.Utils.getDateAltFormats(); 402 break; 403 case "string": 404 if (meta.inputType=='textarea') { 405 field.xtype = meta.xtype || 'textarea'; 406 field.width = meta.width; 407 field.height = meta.height; 408 if (!this._textMeasure) { 409 this._textMeasure = {}; 410 var ta = Ext4.DomHelper.append(document.body,{tag:'textarea', rows:10, cols:80, id:'_hiddenTextArea', style:{display:'none'}}); 411 this._textMeasure.height = Math.ceil(Ext4.util.TextMetrics.measure(ta,"GgYyJjZ==").height * 1.2); 412 this._textMeasure.width = Math.ceil(Ext4.util.TextMetrics.measure(ta,"ABCXYZ").width / 6.0); 413 } 414 if (meta.rows && !meta.height) { 415 if (meta.rows == 1) { 416 field.height = undefined; 417 } 418 else { 419 // estimate at best! 420 var textHeight = this._textMeasure.height * meta.rows; 421 if (textHeight) { 422 field.height = textHeight; 423 } 424 } 425 } 426 if (meta.cols && !meta.width) { 427 var textWidth = this._textMeasure.width * meta.cols; 428 if (textWidth) { 429 field.width = textWidth; 430 } 431 } 432 } 433 else { 434 field.xtype = meta.xtype || 'textfield'; 435 } 436 break; 437 default: 438 field.xtype = meta.xtype || 'textfield'; 439 } 440 } 441 442 return field; 443 }, 444 445 /** 446 * @private 447 * @param col 448 * @param meta 449 * @param grid 450 */ 451 getDefaultRenderer: function(col, meta, grid) { 452 return function(value, cellMetaData, record, rowIndex, colIndex, store) { 453 var displayValue = value; 454 var cellStyles = []; 455 var tdCls = []; 456 457 //format value into a string 458 if(!Ext4.isEmpty(value)) 459 displayValue = Util.getDisplayString(value, meta, record, store); 460 else 461 displayValue = value; 462 463 displayValue = Ext4.util.Format.htmlEncode(displayValue); 464 465 if(meta.buildDisplayString){ 466 displayValue = meta.buildDisplayString({ 467 displayValue: displayValue, 468 value: value, 469 col: col, 470 meta: meta, 471 cellMetaData: cellMetaData, 472 record: record, 473 store: store 474 }); 475 } 476 477 //if meta.file is true, add an <img> for the file icon 478 if (meta.file) { 479 displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(value) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue; 480 //since the icons are 16x16, cut the default padding down to just 1px 481 cellStyles.push('padding: 1px 1px 1px 1px'); 482 } 483 484 //build the URL 485 if(col.showLink !== false){ 486 var url = Util.getColumnUrl(displayValue, value, col, meta, record); 487 if(url){ 488 displayValue = "<a " + (meta.urlTarget ? "target=\""+meta.urlTarget+"\"" : "") + " href=\"" + url + "\">" + displayValue + "</a>"; 489 } 490 } 491 492 if(meta.wordWrap && !col.hidden){ 493 cellStyles.push('white-space:normal !important'); 494 } 495 496 497 if (!record.isValid()){ 498 var errs = record.validate().getByField(meta.name); 499 if (errs.length) 500 tdCls.push('labkey-grid-cell-invalid'); 501 } 502 503 if ((meta.allowBlank === false || meta.nullable === false) && Ext4.isEmpty(value)){ 504 tdCls.push('labkey-grid-cell-invalid'); 505 } 506 507 if(cellStyles.length){ 508 cellMetaData.style = cellMetaData.style ? cellMetaData.style + ';' : ''; 509 cellMetaData.style += (cellStyles.join(';')); 510 } 511 512 if (tdCls.length){ 513 cellMetaData.tdCls = cellMetaData.tdCls ? cellMetaData.tdCls + ' ' : ''; 514 cellMetaData.tdCls += tdCls.join(' '); 515 } 516 517 if (!meta.hasOwnProperty('showTooltip') || meta.showTooltip === true) { 518 Util.buildQtip({ 519 displayValue: displayValue, 520 value: value, 521 meta: meta, 522 col: col, 523 record: record, 524 store: store, 525 cellMetaData: cellMetaData 526 }); 527 } 528 529 return displayValue; 530 } 531 }, 532 533 /** 534 * @private 535 * @param value 536 * @param meta 537 * @param record 538 * @param store 539 */ 540 getDisplayString: function(value, meta, record, store) { 541 var displayType = meta.displayFieldJsonType || meta.jsonType; 542 var displayValue = value; 543 var shouldCache; 544 545 //NOTE: the labkey 9.1 API returns both the value of the field and the display value 546 //the server is already doing the work, so we should rely on this 547 //this does have a few problems: 548 //if the displayValue equals the value, the API omits displayValue. because we cant 549 // count on the server returning the right value unless explicitly providing a displayValue, 550 // we only attempt to use that 551 if(record && record.raw && record.raw[meta.name]){ 552 if(Ext4.isDefined(record.raw[meta.name].displayValue)) 553 return record.raw[meta.name].displayValue; 554 // TODO: this needs testing before enabling. would be nice if we could rely on this, 555 // TODO: but i dont think we will be able to (dates, for example) 556 // perhaps only try this for lookups? 557 //else if(Ext4.isDefined(record.raw[meta.name].value)) 558 // return record.raw[meta.name].value; 559 } 560 561 //NOTE: this is substantially changed over LABKEY.ext.FormHelper 562 if(meta.lookup && meta.lookup['public'] !== false && meta.lookups!==false){ 563 //dont both w/ special renderer if the raw value is the same as the displayColumn 564 if (meta.lookup.keyColumn != meta.lookup.displayColumn){ 565 displayValue = Util.getLookupDisplayValue(meta, displayValue, record, store); 566 meta.usingLookup = true; 567 shouldCache = false; 568 displayType = 'string'; 569 } 570 } 571 572 if(meta.extFormatFn && Ext4.isFunction(meta.extFormatFn)){ 573 displayValue = meta.extFormatFn(displayValue); 574 } 575 else { 576 if(!Ext4.isDefined(displayValue)) 577 displayValue = ''; 578 switch (displayType){ 579 case "date": 580 var date = new Date(displayValue); 581 //NOTE: java formats differ from ext 582 var format = meta.extFormat; 583 if(!format){ 584 if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0) 585 format = "Y-m-d"; 586 else 587 format = "Y-m-d H:i:s"; 588 } 589 displayValue = Ext4.Date.format(date, format); 590 break; 591 case "int": 592 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0'))(displayValue); 593 break; 594 case "boolean": 595 596 var t = meta.editorConfig && meta.editorConfig.trueText ? meta.editorConfig.trueText : (this.trueText || 'true'); 597 var f = meta.editorConfig && meta.editorConfig.falseText ? meta.editorConfig.falseText : (this.falseText || 'false'); 598 var u = meta.editorConfig && meta.editorConfig.undefinedText ? meta.editorConfig.undefinedText : (this.undefinedText || ' '); 599 600 if(displayValue === undefined){ 601 displayValue = u; 602 } 603 else if(!displayValue || displayValue === 'false'){ 604 displayValue = f; 605 } 606 else { 607 displayValue = t; 608 } 609 break; 610 case "float": 611 displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0,000.00'))(displayValue); 612 break; 613 case "string": 614 default: 615 displayValue = !Ext4.isEmpty(displayValue) ? displayValue.toString() : ""; 616 } 617 } 618 619 // Experimental. cache the calculated value, so we dont need to recalculate each time. 620 // This should get cleared by the store on update like any server-generated value 621 if (shouldCache !== false) { 622 record.raw = record.raw || {}; 623 if(!record.raw[meta.name]) 624 record.raw[meta.name] = {}; 625 record.raw[meta.name].displayValue = displayValue; 626 } 627 628 return displayValue; 629 }, 630 631 /** 632 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 633 * The resulting editor is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 634 * contains a formEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 635 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 636 * @param {Object} config as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 637 * @return {Object} An Ext field component 638 */ 639 getFormEditor: function(meta, config) { 640 var editorConfig = Util.getFormEditorConfig(meta, config); 641 return Ext4.ComponentMgr.create(editorConfig); 642 }, 643 644 /** 645 * Return an Ext config object to create an Ext field based on the supplied metadata. 646 * The resulting config object is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata 647 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 648 * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 649 * @param {Object} [config] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}. 650 * @returns {Object} An Ext 4.x config object 651 */ 652 getFormEditorConfig: function(meta, config) { 653 var editor = Util.getDefaultEditorConfig(meta); 654 655 // now we allow overrides of default behavior, in order of precedence 656 if (meta.editorConfig) 657 Ext4.Object.merge(editor, meta.editorConfig); 658 if (meta.formEditorConfig) 659 Ext4.Object.merge(editor, meta.formEditorConfig); 660 if (config) 661 Ext4.Object.merge(editor, config); 662 663 return editor; 664 }, 665 666 /** 667 * Constructs an ext field component based on the supplied metadata. Same as getFormEditorConfig, but actually constructs the editor. 668 * The resulting editor is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 669 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 670 * @private 671 * @param meta 672 * @param config 673 * @return {Object} An Ext field component 674 */ 675 getGridEditor: function(meta, config) { 676 var editorConfig = Util.getGridEditorConfig(meta, config); 677 return Ext4.ComponentMgr.create(editorConfig); 678 }, 679 680 /** 681 * @private 682 * Return an Ext config object to create an Ext field based on the supplied metadata. 683 * The resulting config object is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata 684 * contains a gridEditorConfig property, this config object will be applied to the resulting field. See getDefaultEditorConfig for config options. 685 * 686 * @name getGridEditorConfig 687 * @function 688 * @returns {object} Returns an Ext config object 689 */ 690 getGridEditorConfig: function(meta, config) { 691 //this produces a generic editor 692 var editor = Util.getDefaultEditorConfig(meta); 693 694 //now we allow overrides of default behavior, in order of precedence 695 if (meta.editorConfig) { 696 Ext4.Object.merge(editor, meta.editorConfig); 697 } 698 699 //note: this will screw up cell editors 700 delete editor.fieldLabel; 701 702 if (meta.gridEditorConfig) { 703 Ext4.Object.merge(editor, meta.gridEditorConfig); 704 } 705 if (config) { 706 Ext4.Object.merge(editor, config); 707 } 708 709 return editor; 710 }, 711 712 /** 713 * @private 714 * NOTE: it would be far better if we did not need to pass the store. This is done b/c we need to fire the 715 * 'datachanged' event once the lookup store loads. A better idea would be to force the store/grid to listen 716 * for event fired by the lookupStore or somehow get the metadata to fire events itself. 717 * @param meta 718 * @param data 719 * @param record 720 * @param store 721 */ 722 getLookupDisplayValue: function(meta, data, record, store) { 723 var lookupStore = Util.getLookupStore(meta); 724 if(!lookupStore){ 725 return ''; 726 } 727 728 meta.lookupStore = lookupStore; 729 var lookupRecord; 730 731 //NOTE: preferentially used snapshot instead of data to allow us to find the record even if the store is currently filtered 732 var records = lookupStore.snapshot || lookupStore.data; 733 var matcher = records.createValueMatcher((data == null ? '' : data), false, true, true); 734 var property = meta.lookup.keyColumn; 735 var recIdx = records.findIndexBy(function(o){ 736 return o && matcher.test(o.get(property)); 737 }, null); 738 739 if (recIdx != -1) 740 lookupRecord = records.getAt(recIdx); 741 742 if (lookupRecord){ 743 return lookupRecord.get(meta.lookup.displayColumn); 744 } 745 else { 746 if (data!==null){ 747 return "[" + data + "]"; 748 } 749 else { 750 return Ext4.isDefined(meta.nullCaption) ? meta.nullCaption : "[none]"; 751 } 752 } 753 }, 754 755 /** 756 * @private 757 * @param storeId 758 * @param c 759 */ 760 getLookupStore: function(storeId, c) { 761 if (!Ext4.isString(storeId)) { 762 c = storeId; 763 storeId = Util.getLookupStoreId(c); 764 } 765 766 if (Ext4.isObject(c.store) && c.store.events) { 767 return c.store; 768 } 769 770 var store = Ext4.StoreMgr.lookup(storeId); 771 if (!store) { 772 var config = c.store || Util.getLookupStoreConfig(c); 773 config.storeId = storeId; 774 store = Ext4.create('LABKEY.ext4.data.Store', config); 775 } 776 return store; 777 }, 778 779 /** 780 * @private 781 * @param c 782 */ 783 getLookupStoreConfig: function(c) { 784 var l = c.lookup; 785 786 // normalize lookup 787 l.queryName = l.queryName || l.table; 788 l.schemaName = l.schemaName || l.schema; 789 790 if (l.schemaName == 'core' && l.queryName =='UsersData') { 791 l.queryName = 'Users'; 792 } 793 794 var config = { 795 xtype: "labkeystore", 796 storeId: Util.getLookupStoreId(c), 797 containerFilter: 'CurrentOrParentAndWorkbooks', 798 schemaName: l.schemaName, 799 queryName: l.queryName, 800 containerPath: l.container || l.containerPath || LABKEY.container.path, 801 autoLoad: true 802 }; 803 804 if (l.viewName) { 805 config.viewName = l.viewName; 806 } 807 808 if (l.filterArray) { 809 config.filterArray = l.filterArray; 810 } 811 812 if (l.columns) { 813 config.columns = l.columns; 814 } 815 else { 816 var columns = []; 817 if (l.keyColumn) { 818 columns.push(l.keyColumn); 819 } 820 if (l.displayColumn && l.displayColumn != l.keyColumn) { 821 columns.push(l.displayColumn); 822 } 823 if (columns.length == 0) { 824 columns = ['*']; 825 } 826 config.columns = columns; 827 } 828 829 if (l.sort) { 830 config.sort = l.sort; 831 } 832 else if (l.sort !== false) { 833 config.sort = l.displayColumn; 834 } 835 836 return config; 837 }, 838 839 /** 840 * @private 841 * @param c 842 */ 843 getLookupStoreId: function(c) { 844 if (c.store && c.store.storeId) { 845 return c.store.storeId; 846 } 847 848 if (c.lookup) { 849 return c.lookup.storeId || [ 850 c.lookup.schemaName || c.lookup.schema, 851 c.lookup.queryName || c.lookup.table, 852 c.lookup.keyColumn, 853 c.lookup.displayColumn 854 ].join('||'); 855 } 856 857 return c.name; 858 }, 859 860 /** 861 * @private 862 * EXPERIMENTAL. Returns the fields from the passed store 863 * @param store 864 * @returns {Ext.util.MixedCollection} The fields associated with this store 865 */ 866 getStoreFields: function(store) { 867 return store.proxy.reader.model.prototype.fields; 868 }, 869 870 /** 871 * @private 872 * @param store 873 * @return {boolean} Whether the store has loaded 874 */ 875 hasStoreLoaded: function(store) { 876 return store.proxy && store.proxy.reader && store.proxy.reader.rawData; 877 }, 878 879 /** 880 * @private 881 * Identify the proper name of a field using an input string such as an excel column label. This helper will 882 * perform a case-insensitive comparison of the field name, label, caption, shortCaption and aliases. 883 * @param {string} fieldName The string to search 884 * @param {Array/Ext.util.MixedCollection} metadata The fields to search 885 * @return {string} The normalized field name or null if not found 886 */ 887 resolveFieldNameFromLabel: function(fieldName, meta) { 888 var fnMatch = []; 889 var aliasMatch = []; 890 891 var testField = function(fieldMeta) { 892 if (caseInsensitiveEquals(fieldName, fieldMeta.name) 893 || caseInsensitiveEquals(fieldName, fieldMeta.caption) 894 || caseInsensitiveEquals(fieldName, fieldMeta.shortCaption) 895 || caseInsensitiveEquals(fieldName, fieldMeta.label) 896 ){ 897 fnMatch.push(fieldMeta.name); 898 return false; //exit here because it should only match 1 name 899 } 900 901 if (fieldMeta.importAliases) { 902 var aliases; 903 if(Ext4.isArray(fieldMeta.importAliases)) 904 aliases = fieldMeta.importAliases; 905 else 906 aliases = fieldMeta.importAliases.split(','); 907 908 Ext4.each(aliases, function(alias){ 909 if (caseInsensitiveEquals(fieldName, alias)) 910 aliasMatch.push(fieldMeta.name); //continue iterating over fields in case a fieldName matches 911 }, this); 912 } 913 }; 914 915 if (meta.hasOwnProperty('each')) { 916 meta.each(testField, this); 917 } 918 else { 919 Ext4.each(meta, testField, this); 920 } 921 922 if (fnMatch.length==1) { 923 return fnMatch[0]; 924 } 925 else if (fnMatch.length > 1) { 926 return null; 927 } 928 else if (aliasMatch.length==1) { 929 return aliasMatch[0]; 930 } 931 return null; 932 }, 933 934 /** 935 * @private 936 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in a details view. 937 * If any of the following are true, it will not appear: hidden, isHidden 938 * If shownInDetailsView is defined, it will take priority 939 * @param {Object} metadata The field metadata object 940 * @return {boolean} Whether the field should appear in the default details view 941 */ 942 shouldShowInDetailsView: function(metadata){ 943 return Ext4.isDefined(metadata.shownInDetailsView) ? metadata.shownInDetailsView : 944 (!metadata.isHidden && !metadata.hidden && metadata.shownInDetailsView!==false); 945 }, 946 947 /** 948 * @private 949 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an insert view. 950 * If any of the following are false, it will not appear: userEditable and autoIncrement 951 * If any of the follow are true, it will not appear: hidden, isHidden 952 * If shownInInsertView is defined, this will take priority over all 953 * @param {Object} metadata The field metadata object 954 * @return {boolean} Whether the field should appear in the default insert view 955 */ 956 shouldShowInInsertView: function(metadata){ 957 return Ext4.isDefined(metadata.shownInInsertView) ? metadata.shownInInsertView : 958 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement); 959 }, 960 961 /** 962 * @private 963 * EXPERIMENTAL. Provides a consistent implementation for determining whether a field should appear in an update view. 964 * If any of the following are false, it will not appear: userEditable and autoIncrement 965 * If any of the follow are true, it will not appear: hidden, isHidden, readOnly 966 * If shownInUpdateView is defined, this will take priority over all 967 * @param {Object} metadata The field metadata object 968 * @return {boolean} Whether the field should appear 969 */ 970 shouldShowInUpdateView: function(metadata) { 971 return Ext4.isDefined(metadata.shownInUpdateView) ? metadata.shownInUpdateView : 972 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement && metadata.readOnly!==false) 973 }, 974 975 /** 976 * @private 977 * Shortcut for LABKEY.ext4.Util.getLookupStore that doesn't require as complex a config object 978 * @param {Object} config Configuration object for an Ext.data.Store 979 * @return {Ext.data.Store} The store 980 */ 981 simpleLookupStore: function(config) { 982 config.lookup = { 983 containerPath : config.containerPath, 984 schemaName : config.schemaName, 985 queryName : config.queryName, 986 viewName : config.viewName, 987 displayColumn : config.displayColumn, 988 keyColumn : config.keyColumn 989 }; 990 991 return Util.getLookupStore(config); 992 }, 993 994 /** 995 * @private 996 * The intention of this method is to provide a standard, low-level way to translating Labkey metadata names into ext ones. 997 * @param field 998 */ 999 translateMetadata: function(field) { 1000 field.fieldLabel = Ext4.util.Format.htmlEncode(field.label || field.caption || field.header || field.name); 1001 field.dataIndex = field.dataIndex || field.name; 1002 field.editable = (field.userEditable!==false && !field.readOnly && !field.autoIncrement && !field.calculated); 1003 field.allowBlank = (field.nullable === true) || (field.required !== true); 1004 field.jsonType = field.jsonType || Util.findJsonType(field); 1005 1006 //this will convert values from strings to the correct type (such as booleans) 1007 if (!Ext4.isEmpty(field.defaultValue)){ 1008 var type = Ext4.data.Types[LABKEY.ext4.Util.EXT_TYPE_MAP[field.jsonType]]; 1009 if (type){ 1010 field.defaultValue = type.convert(field.defaultValue); 1011 } 1012 } 1013 }, 1014 1015 /** 1016 * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and 1017 * resizes it so the Container fits inside the viewable region of the window. This is generally used in the case 1018 * where the Container is not rendered to a webpart but rather displayed on the page itself (e.g. SchemaBrowser, 1019 * manageFolders, etc). 1020 * @param extContainer - (Required) outer container which is the target to be resized 1021 * @param width - (Required) width of the viewport. In many cases, the window width. If a negative width is passed than 1022 * the width will not be set. 1023 * @param height - (Required) height of the viewport. In many cases, the window height. If a negative height is passed than 1024 * the height will not be set. 1025 * @param paddingX - distance from the right edge of the viewport. Defaults to 35. 1026 * @param paddingY - distance from the bottom edge of the viewport. Defaults to 35. 1027 */ 1028 resizeToViewport: function(extContainer, width, height, paddingX, paddingY, offsetX, offsetY) 1029 { 1030 LABKEY.ext4.Util.resizeToContainer.apply(this, arguments); 1031 } 1032 }); 1033 }()); 1034