1 /*
  2  * Copyright (c) 2012-2018 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             }
374             else {
375                 switch (meta.jsonType) {
376                     case "boolean":
377                         field.xtype = meta.xtype || 'checkbox';
378                             if (field.value === true){
379                                 field.checked = true;
380                             }
381                         break;
382                     case "int":
383                         field.xtype = meta.xtype || 'numberfield';
384                         field.allowDecimals = false;
385                         break;
386                     case "float":
387                         field.xtype = meta.xtype || 'numberfield';
388                         field.allowDecimals = true;
389                         break;
390                     case "date":
391                         field.xtype = meta.xtype || 'datefield';
392                         field.format = meta.extFormat || Date.patterns.ISO8601Long;
393                         field.altFormats = LABKEY.Utils.getDateAltFormats();
394                         break;
395                     case "string":
396                         if (meta.inputType=='textarea') {
397                             field.xtype = meta.xtype || 'textarea';
398                             field.width = meta.width;
399                             field.height = meta.height;
400                             if (!this._textMeasure) {
401                                 this._textMeasure = {};
402                                 var ta = Ext4.DomHelper.append(document.body,{tag:'textarea', rows:10, cols:80, id:'_hiddenTextArea', style:{display:'none'}});
403                                 this._textMeasure.height = Math.ceil(Ext4.util.TextMetrics.measure(ta,"GgYyJjZ==").height * 1.2);
404                                 this._textMeasure.width  = Math.ceil(Ext4.util.TextMetrics.measure(ta,"ABCXYZ").width / 6.0);
405                             }
406                             if (meta.rows && !meta.height) {
407                                 if (meta.rows == 1) {
408                                     field.height = undefined;
409                                 }
410                                 else {
411                                     // estimate at best!
412                                     var textHeight = this._textMeasure.height * meta.rows;
413                                     if (textHeight) {
414                                         field.height = textHeight;
415                                     }
416                                 }
417                             }
418                             if (meta.cols && !meta.width) {
419                                 var textWidth = this._textMeasure.width * meta.cols;
420                                 if (textWidth) {
421                                     field.width = textWidth;
422                                 }
423                             }
424                         }
425                         else {
426                             field.xtype = meta.xtype || 'textfield';
427                         }
428                         break;
429                     default:
430                         field.xtype = meta.xtype || 'textfield';
431                 }
432             }
433 
434             return field;
435         },
436 
437         /**
438          * @private
439          * @param col
440          * @param meta
441          * @param grid
442          */
443         getDefaultRenderer: function(col, meta, grid) {
444             return function(value, cellMetaData, record, rowIndex, colIndex, store) {
445                 var displayValue = value;
446                 var cellStyles = [];
447                 var tdCls = [];
448 
449                 //format value into a string
450                 if(!Ext4.isEmpty(value))
451                     displayValue = Util.getDisplayString(value, meta, record, store);
452                 else
453                     displayValue = value;
454 
455                 displayValue = Ext4.util.Format.htmlEncode(displayValue);
456 
457                 if(meta.buildDisplayString){
458                     displayValue = meta.buildDisplayString({
459                         displayValue: displayValue,
460                         value: value,
461                         col: col,
462                         meta: meta,
463                         cellMetaData: cellMetaData,
464                         record: record,
465                         store: store
466                     });
467                 }
468 
469                 //if meta.file is true, add an <img> for the file icon
470                 if (meta.file) {
471                     displayValue = "<img src=\"" + LABKEY.Utils.getFileIconUrl(value) + "\" alt=\"icon\" title=\"Click to download file\"/> " + displayValue;
472                     //since the icons are 16x16, cut the default padding down to just 1px
473                     cellStyles.push('padding: 1px 1px 1px 1px');
474                 }
475 
476                 //build the URL
477                 if(col.showLink !== false){
478                     var url = Util.getColumnUrl(displayValue, value, col, meta, record);
479                     if(url){
480                         displayValue = "<a " + (meta.urlTarget ? "target=\""+meta.urlTarget+"\"" : "") + " href=\"" + url + "\">" + displayValue + "</a>";
481                     }
482                 }
483 
484                 if(meta.wordWrap && !col.hidden){
485                     cellStyles.push('white-space:normal !important');
486                 }
487 
488 
489                 if (!record.isValid()){
490                     var errs = record.validate().getByField(meta.name);
491                     if (errs.length)
492                         tdCls.push('labkey-grid-cell-invalid');
493                 }
494 
495                 if ((meta.allowBlank === false || meta.nullable === false) && Ext4.isEmpty(value)){
496                     tdCls.push('labkey-grid-cell-invalid');
497                 }
498 
499                 if(cellStyles.length){
500                     cellMetaData.style = cellMetaData.style ? cellMetaData.style + ';' : '';
501                     cellMetaData.style += (cellStyles.join(';'));
502                 }
503 
504                 if (tdCls.length){
505                     cellMetaData.tdCls = cellMetaData.tdCls ? cellMetaData.tdCls + ' ' : '';
506                     cellMetaData.tdCls += tdCls.join(' ');
507                 }
508 
509                 if (!meta.hasOwnProperty('showTooltip') || meta.showTooltip === true) {
510                     Util.buildQtip({
511                         displayValue: displayValue,
512                         value: value,
513                         meta: meta,
514                         col: col,
515                         record: record,
516                         store: store,
517                         cellMetaData: cellMetaData
518                     });
519                 }
520 
521                 return displayValue;
522             }
523         },
524 
525         /**
526          * @private
527          * @param value
528          * @param meta
529          * @param record
530          * @param store
531          */
532         getDisplayString: function(value, meta, record, store) {
533             var displayType = meta.displayFieldJsonType || meta.jsonType;
534             var displayValue = value;
535             var shouldCache;
536 
537             //NOTE: the labkey 9.1 API returns both the value of the field and the display value
538             //the server is already doing the work, so we should rely on this
539             //this does have a few problems:
540             //if the displayValue equals the value, the API omits displayValue.  because we cant
541             // count on the server returning the right value unless explicitly providing a displayValue,
542             // we only attempt to use that
543             if(record && record.raw && record.raw[meta.name]){
544                 if(Ext4.isDefined(record.raw[meta.name].displayValue))
545                     return record.raw[meta.name].displayValue;
546                 // TODO: this needs testing before enabling.  would be nice if we could rely on this,
547                 // TODO: but i dont think we will be able to (dates, for example)
548                 // perhaps only try this for lookups?
549                 //else if(Ext4.isDefined(record.raw[meta.name].value))
550                 //    return record.raw[meta.name].value;
551             }
552 
553             //NOTE: this is substantially changed over LABKEY.ext.FormHelper
554             if(meta.lookup && meta.lookup['public'] !== false && meta.lookups!==false){
555                 //dont both w/ special renderer if the raw value is the same as the displayColumn
556                 if (meta.lookup.keyColumn != meta.lookup.displayColumn){
557                     displayValue = Util.getLookupDisplayValue(meta, displayValue, record, store);
558                     meta.usingLookup = true;
559                     shouldCache = false;
560                     displayType = 'string';
561                 }
562             }
563 
564             if(meta.extFormatFn && Ext4.isFunction(meta.extFormatFn)){
565                 displayValue = meta.extFormatFn(displayValue);
566             }
567             else {
568                 if(!Ext4.isDefined(displayValue))
569                     displayValue = '';
570                 switch (displayType){
571                     case "date":
572                         var date = new Date(displayValue);
573                         //NOTE: java formats differ from ext
574                         var format = meta.extFormat;
575                         if(!format){
576                             if (date.getHours() == 0 && date.getMinutes() == 0 && date.getSeconds() == 0)
577                                 format = "Y-m-d";
578                             else
579                                 format = "Y-m-d H:i:s";
580                         }
581                         displayValue = Ext4.Date.format(date, format);
582                         break;
583                     case "int":
584                         displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0'))(displayValue);
585                         break;
586                     case "boolean":
587 
588                         var t = meta.editorConfig && meta.editorConfig.trueText ? meta.editorConfig.trueText : (this.trueText || 'true');
589                         var f = meta.editorConfig && meta.editorConfig.falseText ? meta.editorConfig.falseText : (this.falseText || 'false');
590                         var u = meta.editorConfig && meta.editorConfig.undefinedText ? meta.editorConfig.undefinedText : (this.undefinedText || ' ');
591 
592                         if(displayValue === undefined){
593                             displayValue = u;
594                         }
595                         else if(!displayValue || displayValue === 'false'){
596                             displayValue = f;
597                         }
598                         else {
599                             displayValue = t;
600                         }
601                         break;
602                     case "float":
603                         displayValue = (Ext4.util.Format.numberRenderer(meta.extFormat || this.format || '0,000.00'))(displayValue);
604                         break;
605                     case "string":
606                     default:
607                         displayValue = !Ext4.isEmpty(displayValue) ? displayValue.toString() : "";
608                 }
609             }
610 
611             // Experimental. cache the calculated value, so we dont need to recalculate each time.
612             // This should get cleared by the store on update like any server-generated value
613             if (shouldCache !== false) {
614                 record.raw = record.raw || {};
615                 if(!record.raw[meta.name])
616                     record.raw[meta.name] = {};
617                 record.raw[meta.name].displayValue = displayValue;
618             }
619 
620             return displayValue;
621         },
622 
623         /**
624          * Constructs an ext field component based on the supplied metadata.  Same as getFormEditorConfig, but actually constructs the editor.
625          * The resulting editor is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata
626          * contains a formEditorConfig property, this config object will be applied to the resulting field.  See getDefaultEditorConfig for config options.
627          * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
628          * @param {Object} config as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
629          * @return {Object} An Ext field component
630          */
631         getFormEditor: function(meta, config) {
632             var editorConfig = Util.getFormEditorConfig(meta, config);
633             return Ext4.ComponentMgr.create(editorConfig);
634         },
635 
636         /**
637          * Return an Ext config object to create an Ext field based on the supplied metadata.
638          * The resulting config object is tailored for usage in a form, as opposed to a grid. Unlike getEditorConfig, if the metadata
639          * contains a gridEditorConfig property, this config object will be applied to the resulting field.  See getDefaultEditorConfig for config options.
640          * @param {Object} meta as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
641          * @param {Object} [config] as returned by {@link LABKEY.Query.selectRows}. See {@link LABKEY.Query.SelectRowsResults}.
642          * @returns {Object} An Ext 4.x config object
643          */
644         getFormEditorConfig: function(meta, config) {
645             var editor = Util.getDefaultEditorConfig(meta);
646 
647             // now we allow overrides of default behavior, in order of precedence
648             if (meta.editorConfig)
649                 Ext4.Object.merge(editor, meta.editorConfig);
650             if (meta.formEditorConfig)
651                 Ext4.Object.merge(editor, meta.formEditorConfig);
652             if (config)
653                 Ext4.Object.merge(editor, config);
654 
655             return editor;
656         },
657 
658         /**
659          * Constructs an ext field component based on the supplied metadata.  Same as getFormEditorConfig, but actually constructs the editor.
660          * The resulting editor is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata
661          * contains a gridEditorConfig property, this config object will be applied to the resulting field.  See getDefaultEditorConfig for config options.
662          * @private
663          * @param meta
664          * @param config
665          * @return {Object} An Ext field component
666          */
667         getGridEditor: function(meta, config) {
668             var editorConfig = Util.getGridEditorConfig(meta, config);
669             return Ext4.ComponentMgr.create(editorConfig);
670         },
671 
672         /**
673          * @private
674          * Return an Ext config object to create an Ext field based on the supplied metadata.
675          * The resulting config object is tailored for usage in a grid, as opposed to a form. Unlike getFormEditorConfig or getEditorConfig, if the metadata
676          * contains a gridEditorConfig property, this config object will be applied to the resulting field.  See getDefaultEditorConfig for config options.
677          *
678          * @name getGridEditorConfig
679          * @function
680          * @returns {object} Returns an Ext config object
681          */
682         getGridEditorConfig: function(meta, config) {
683             //this produces a generic editor
684             var editor = Util.getDefaultEditorConfig(meta);
685 
686             //now we allow overrides of default behavior, in order of precedence
687             if (meta.editorConfig) {
688                 Ext4.Object.merge(editor, meta.editorConfig);
689             }
690 
691             //note: this will screw up cell editors
692             delete editor.fieldLabel;
693 
694             if (meta.gridEditorConfig) {
695                 Ext4.Object.merge(editor, meta.gridEditorConfig);
696             }
697             if (config) {
698                 Ext4.Object.merge(editor, config);
699             }
700 
701             return editor;
702         },
703 
704         /**
705          * @private
706          * 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
707          * 'datachanged' event once the lookup store loads. A better idea would be to force the store/grid to listen
708          * for event fired by the lookupStore or somehow get the metadata to fire events itself.
709          * @param meta
710          * @param data
711          * @param record
712          * @param store
713          */
714         getLookupDisplayValue: function(meta, data, record, store) {
715             var lookupStore = Util.getLookupStore(meta);
716             if(!lookupStore){
717                 return '';
718             }
719 
720             meta.lookupStore = lookupStore;
721             var lookupRecord;
722 
723             //NOTE: preferentially used snapshot instead of data to allow us to find the record even if the store is currently filtered
724             var records = lookupStore.snapshot || lookupStore.data;
725             var matcher = records.createValueMatcher((data == null ? '' : data), false, true, true);
726             var property = meta.lookup.keyColumn;
727             var recIdx = records.findIndexBy(function(o){
728                 return o && matcher.test(o.get(property));
729             }, null);
730 
731             if (recIdx != -1)
732                 lookupRecord = records.getAt(recIdx);
733 
734             if (lookupRecord){
735                 return lookupRecord.get(meta.lookup.displayColumn);
736             }
737             else {
738                 if (data!==null){
739                     return "[" + data + "]";
740                 }
741                 else {
742                     return Ext4.isDefined(meta.nullCaption) ? meta.nullCaption : "[none]";
743                 }
744             }
745         },
746 
747         /**
748          * @private
749          * @param storeId
750          * @param c
751          */
752         getLookupStore: function(storeId, c) {
753             if (!Ext4.isString(storeId)) {
754                 c = storeId;
755                 storeId = Util.getLookupStoreId(c);
756             }
757 
758             if (Ext4.isObject(c.store) && c.store.events) {
759                 return c.store;
760             }
761 
762             var store = Ext4.StoreMgr.lookup(storeId);
763             if (!store) {
764                 var config = c.store || Util.getLookupStoreConfig(c);
765                 config.storeId = storeId;
766                 store = Ext4.create('LABKEY.ext4.data.Store', config);
767             }
768             return store;
769         },
770 
771         /**
772          * @private
773          * @param c
774          */
775         getLookupStoreConfig: function(c) {
776             var l = c.lookup;
777 
778             // normalize lookup
779             l.queryName = l.queryName || l.table;
780             l.schemaName = l.schemaName || l.schema;
781 
782             if (l.schemaName == 'core' && l.queryName =='UsersData') {
783                 l.queryName = 'Users';
784             }
785 
786             var config = {
787                 xtype: "labkeystore",
788                 storeId: Util.getLookupStoreId(c),
789                 containerFilter: 'CurrentOrParentAndWorkbooks',
790                 schemaName: l.schemaName,
791                 queryName: l.queryName,
792                 containerPath: l.container || l.containerPath || LABKEY.container.path,
793                 autoLoad: true
794             };
795 
796             if (l.viewName) {
797                 config.viewName = l.viewName;
798             }
799 
800             if (l.filterArray) {
801                 config.filterArray = l.filterArray;
802             }
803 
804             if (l.columns) {
805                 config.columns = l.columns;
806             }
807             else {
808                 var columns = [];
809                 if (l.keyColumn) {
810                     columns.push(l.keyColumn);
811                 }
812                 if (l.displayColumn && l.displayColumn != l.keyColumn) {
813                     columns.push(l.displayColumn);
814                 }
815                 if (columns.length == 0) {
816                     columns = ['*'];
817                 }
818                 config.columns = columns;
819             }
820 
821             if (l.sort) {
822                 config.sort = l.sort;
823             }
824             else if (l.sort !== false) {
825                 config.sort = l.displayColumn;
826             }
827 
828             return config;
829         },
830 
831         /**
832          * @private
833          * @param c
834          */
835         getLookupStoreId: function(c) {
836             if (c.store && c.store.storeId) {
837                 return c.store.storeId;
838             }
839 
840             if (c.lookup) {
841                 return c.lookup.storeId || [
842                     c.lookup.schemaName || c.lookup.schema,
843                     c.lookup.queryName || c.lookup.table,
844                     c.lookup.keyColumn,
845                     c.lookup.displayColumn
846                 ].join('||');
847             }
848 
849             return c.name;
850         },
851 
852         /**
853          * @private
854          * EXPERIMENTAL.  Returns the fields from the passed store
855          * @param store
856          * @returns {Ext.util.MixedCollection} The fields associated with this store
857          */
858         getStoreFields: function(store) {
859             return store.proxy.reader.model.prototype.fields;
860         },
861 
862         /**
863          * @private
864          * @param store
865          * @return {boolean} Whether the store has loaded
866          */
867         hasStoreLoaded: function(store) {
868             return store.proxy && store.proxy.reader && store.proxy.reader.rawData;
869         },
870 
871         /**
872          * @private
873          * Identify the proper name of a field using an input string such as an excel column label.  This helper will
874          * perform a case-insensitive comparison of the field name, label, caption, shortCaption and aliases.
875          * @param {string} fieldName The string to search
876          * @param {Array/Ext.util.MixedCollection} metadata The fields to search
877          * @return {string} The normalized field name or null if not found
878          */
879         resolveFieldNameFromLabel: function(fieldName, meta) {
880             var fnMatch = [];
881             var aliasMatch = [];
882 
883             var testField = function(fieldMeta) {
884                 if (caseInsensitiveEquals(fieldName, fieldMeta.name)
885                     || caseInsensitiveEquals(fieldName, fieldMeta.caption)
886                     || caseInsensitiveEquals(fieldName, fieldMeta.shortCaption)
887                     || caseInsensitiveEquals(fieldName, fieldMeta.label)
888                 ){
889                     fnMatch.push(fieldMeta.name);
890                     return false;  //exit here because it should only match 1 name
891                 }
892 
893                 if (fieldMeta.importAliases) {
894                     var aliases;
895                     if(Ext4.isArray(fieldMeta.importAliases))
896                         aliases = fieldMeta.importAliases;
897                     else
898                         aliases = fieldMeta.importAliases.split(',');
899 
900                     Ext4.each(aliases, function(alias){
901                         if (caseInsensitiveEquals(fieldName, alias))
902                             aliasMatch.push(fieldMeta.name);  //continue iterating over fields in case a fieldName matches
903                     }, this);
904                 }
905             };
906 
907             if (meta.hasOwnProperty('each')) {
908                 meta.each(testField, this);
909             }
910             else {
911                 Ext4.each(meta, testField, this);
912             }
913 
914             if (fnMatch.length==1) {
915                 return fnMatch[0];
916             }
917             else if (fnMatch.length > 1) {
918                 return null;
919             }
920             else if (aliasMatch.length==1) {
921                 return aliasMatch[0];
922             }
923             return null;
924         },
925 
926         /**
927          * @private
928          * EXPERIMENTAL.  Provides a consistent implementation for determining whether a field should appear in a details view.
929          * If any of the following are true, it will not appear: hidden, isHidden
930          * If shownInDetailsView is defined, it will take priority
931          * @param {Object} metadata The field metadata object
932          * @return {boolean} Whether the field should appear in the default details view
933          */
934         shouldShowInDetailsView: function(metadata){
935             return Ext4.isDefined(metadata.shownInDetailsView) ? metadata.shownInDetailsView :
936                 (!metadata.isHidden && !metadata.hidden && metadata.shownInDetailsView!==false);
937         },
938 
939         /**
940          * @private
941          * EXPERIMENTAL.  Provides a consistent implementation for determining whether a field should appear in an insert view.
942          * If any of the following are false, it will not appear: userEditable and autoIncrement
943          * If any of the follow are true, it will not appear: hidden, isHidden
944          * If shownInInsertView is defined, this will take priority over all
945          * @param {Object} metadata The field metadata object
946          * @return {boolean} Whether the field should appear in the default insert view
947          */
948         shouldShowInInsertView: function(metadata){
949             return Ext4.isDefined(metadata.shownInInsertView) ?  metadata.shownInInsertView :
950                 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement);
951         },
952 
953         /**
954          * @private
955          * EXPERIMENTAL.  Provides a consistent implementation for determining whether a field should appear in an update view.
956          * If any of the following are false, it will not appear: userEditable and autoIncrement
957          * If any of the follow are true, it will not appear: hidden, isHidden, readOnly
958          * If shownInUpdateView is defined, this will take priority over all
959          * @param {Object} metadata The field metadata object
960          * @return {boolean} Whether the field should appear
961          */
962         shouldShowInUpdateView: function(metadata) {
963             return Ext4.isDefined(metadata.shownInUpdateView) ? metadata.shownInUpdateView :
964                 (!metadata.calculated && !metadata.isHidden && !metadata.hidden && metadata.userEditable!==false && !metadata.autoIncrement && metadata.readOnly!==false)
965         },
966 
967         /**
968          * @private
969          * Shortcut for LABKEY.ext4.Util.getLookupStore that doesn't require as complex a config object
970          * @param {Object} config Configuration object for an Ext.data.Store
971          * @return {Ext.data.Store} The store
972          */
973         simpleLookupStore: function(config) {
974             config.lookup = {
975                 containerPath : config.containerPath,
976                 schemaName    : config.schemaName,
977                 queryName     : config.queryName,
978                 viewName      : config.viewName,
979                 displayColumn : config.displayColumn,
980                 keyColumn     : config.keyColumn
981             };
982 
983             return Util.getLookupStore(config);
984         },
985 
986         /**
987          * @private
988          * The intention of this method is to provide a standard, low-level way to translating Labkey metadata names into ext ones.
989          * @param field
990          */
991         translateMetadata: function(field) {
992             field.fieldLabel = Ext4.util.Format.htmlEncode(field.label || field.caption || field.header || field.name);
993             field.dataIndex  = field.dataIndex || field.name;
994             field.editable   = (field.userEditable!==false && !field.readOnly && !field.autoIncrement && !field.calculated);
995             field.allowBlank = (field.nullable === true) || (field.required !== true);
996             field.jsonType   = field.jsonType || Util.findJsonType(field);
997 
998             //this will convert values from strings to the correct type (such as booleans)
999             if (!Ext4.isEmpty(field.defaultValue)){
1000                 var type = Ext4.data.Types[LABKEY.ext4.Util.EXT_TYPE_MAP[field.jsonType]];
1001                 if (type){
1002                     field.defaultValue = type.convert(field.defaultValue);
1003                 }
1004             }
1005         },
1006 
1007         /**
1008          * This method takes an object that is/extends an Ext4.Container (e.g. Panels, Toolbars, Viewports, Menus) and
1009          * resizes it so the Container fits inside the viewable region of the window. This is generally used in the case
1010          * where the Container is not rendered to a webpart but rather displayed on the page itself (e.g. SchemaBrowser,
1011          * manageFolders, etc).
1012          * @param extContainer - (Required) outer container which is the target to be resized
1013          * @param width - (Required) width of the viewport. In many cases, the window width. If a negative width is passed than
1014          *                           the width will not be set.
1015          * @param height - (Required) height of the viewport. In many cases, the window height. If a negative height is passed than
1016          *                           the height will not be set.
1017          * @param paddingX - distance from the right edge of the viewport. Defaults to 35.
1018          * @param paddingY - distance from the bottom edge of the viewport. Defaults to 35.
1019          */
1020         resizeToViewport: function(extContainer, width, height, paddingX, paddingY, offsetX, offsetY)
1021         {
1022             LABKEY.ext4.Util.resizeToContainer.apply(this, arguments);
1023         }
1024     });
1025 }());
1026