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