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