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