Fix generic grid model and renderer for virtual fields
[tine20] / tine20 / Tinebase / js / ApplicationStarter.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Tinebase
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Alexander Stintzing <a.stintzing@metaways.de>
7  * @copyright   Copyright (c) 2012-2013 Metaways Infosystems GmbH (http://www.metaways.de)
8  *
9  */
10 Ext.namespace('Tine.Tinebase');
11
12 /**
13  * Tinebase Application Starter
14  * 
15  * @namespace   Tine.Tinebase
16  * @function    Tine.MailAccounting.MailAggregateGridPanel
17  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
18  * @author      Alexander Stintzing <a.stintzing@metaways.de>
19  */
20 Tine.Tinebase.ApplicationStarter = {
21     
22     /**
23      * the applictions the user has access to
24      * @type 
25      */
26     userApplications: null,
27     
28     /**
29      * type mapping
30      * @type {Object}
31      */
32     types: {
33         'date':     'date',
34         'datetime': 'date',
35         'time':     'date',
36         'string':   'string',
37         'text':     'string',
38         'boolean':  'bool',
39         'integer':  'int',
40         'float':    'float'
41     },
42     
43     /**
44      * initializes the starter
45      */
46     init: function() {
47         // Wait until appmgr is initialized
48         if (! Tine.Tinebase.hasOwnProperty('appMgr')) {
49             this.init.defer(100, this);
50             return;
51         }
52         
53         Tine.log.info('ApplicationStarter::init');
54         
55         if (! this.userApplications || this.userApplications.length == 0) {
56             this.userApplications = Tine.Tinebase.registry.get('userApplications');
57             this.createStructure(true);
58         }
59     },
60     
61     /**
62      * returns the field
63      * 
64      * @param {Object} fieldDefinition
65      * @return {Object}
66      */
67     getField: function(fieldDefinition, key) {
68         // default type is auto
69         var field = {name: key};
70         
71         if (fieldDefinition.type) {
72             // add pre defined type
73             field.type = this.types[fieldDefinition.type];
74             switch (fieldDefinition.type) {
75                 case 'datetime':
76                     field.dateFormat = Date.patterns.ISO8601Long;
77                     break;
78                 case 'date':
79                     field.dateFormat = Date.patterns.ISO8601Long;
80                     break;
81                 case 'time':
82                     field.dateFormat = Date.patterns.ISO8601Time;
83                     break;
84                 case 'record':
85                 case 'records':
86                     fieldDefinition.config.modelName = fieldDefinition.config.modelName.replace(/_/, '');
87                     field.type = fieldDefinition.config.appName + '.' + fieldDefinition.config.modelName;
88                     break;
89             }
90             // allow overwriting date pattern in model
91             if (fieldDefinition.hasOwnProperty('dateFormat')) {
92                 field.dateFormat = fieldDefinition.dateFormat;
93             }
94             
95             if (fieldDefinition.hasOwnProperty('label')) {
96                 field.label = fieldDefinition.label;
97             }
98         }
99         
100         // TODO: create field registry, add fields here
101         return field;
102     },
103     /**
104      * returns the grid renderer
105      * @param {Object} config
106      * @param {String} field
107      * @return {Function}
108      */
109     getGridRenderer: function(config, field, appName, modelName) {
110         var gridRenderer = null;
111         if (config && field) {
112             switch (config.type) {
113                 case 'record':
114                     if (Tine.Tinebase.common.hasRight('view', config.config.appName, config.config.modelName.toLowerCase())) {
115                         if (config.config.appName == appName && config.config.modelName == modelName) {
116                             // pointing to same model
117                             gridRenderer = function (value, row, record) {
118                                 var title = value && config.config.titleProperty ? value[config.config.titleProperty] : '';
119                                 return Ext.util.Format.htmlEncode(title);
120                             };
121                         } else {
122                             var foreignRecordClass = Tine[config.config.appName].Model[config.config.modelName];
123                             if (foreignRecordClass) {
124                                 gridRenderer = function (value, row, record) {
125                                     var titleProperty = foreignRecordClass.getMeta('titleProperty');
126                                     return record && record.get(field) ? Ext.util.Format.htmlEncode(record.get(field)[titleProperty]) : '';
127                                 };
128                             } else {
129                                 gridRenderer = null;
130                             }
131                         }
132                     } else {
133                         gridRenderer = null;
134                     }
135                     break;
136                 case 'integer':
137                 case 'float':
138                     if (config.hasOwnProperty('specialType')) {
139                         switch (config.specialType) {
140                             case 'bytes1000':
141                                 gridRenderer = function(value, cell, record) {
142                                     return Tine.Tinebase.common.byteRenderer(value, cell, record, 2, true);
143                                 };
144                                 break;
145                             case 'bytes':
146                                 gridRenderer = function(value, cell, record) {
147                                     return Tine.Tinebase.common.byteRenderer(value, cell, record, 2, false);
148                                 };
149                                 break;
150                             case 'minutes':
151                                 gridRenderer = Tine.Tinebase.common.minutesRenderer;
152                                 break;
153                             case 'seconds':
154                                 gridRenderer = Tine.Tinebase.common.secondsRenderer;
155                                 break;
156                             case 'percent':
157                                 gridRenderer = function(value, cell, record) {
158                                     return Tine.Tinebase.common.percentRenderer(value, config.type);
159                                 };
160                                 break;
161                             default:
162                                 gridRenderer = Ext.util.Format.htmlEncode;
163                         }
164                     }
165                     break;
166                 case 'user':
167                     gridRenderer = Tine.Tinebase.common.usernameRenderer;
168                     break;
169                 case 'keyfield': 
170                     gridRenderer = Tine.Tinebase.widgets.keyfield.Renderer.get(appName, config.name);
171                     break;
172                 case 'date':
173                     gridRenderer = Tine.Tinebase.common.dateRenderer;
174                     break;
175                 case 'datetime':
176                     gridRenderer = Tine.Tinebase.common.dateTimeRenderer;
177                     break;
178                 case 'time':
179                     gridRenderer = Tine.Tinebase.common.timeRenderer;
180                     break;
181                 case 'tag':
182                     gridRenderer = Tine.Tinebase.common.tagsRenderer;
183                     break;
184                 case 'container':
185                     gridRenderer = Tine.Tinebase.common.containerRenderer;
186                     break;
187                 case 'boolean':
188                     gridRenderer = Tine.Tinebase.common.booleanRenderer;
189                     break;
190                 case 'money':
191                     gridRenderer = Ext.util.Format.money;
192                     break;
193                 case 'relation':
194                     var cc = config.config;
195                     gridRenderer = new Tine.widgets.relation.GridRenderer({
196                         appName: appName,
197                         type: cc.type,
198                         foreignApp: cc.appName,
199                         foreignModel: cc.modelName
200                         });
201                     break;
202                 default:
203                     gridRenderer = Ext.util.Format.htmlEncode;
204             }
205         }
206         return gridRenderer;
207     },
208
209     /**
210      * used in getFilter for mapping types to filter
211      * 
212      * @type 
213      */
214     filterMap: function(type, fieldconfig, filter, filterconfig, appName, modelName, modelConfig) {
215         switch (type) {
216             case 'string':
217             case 'text':
218                 break;
219             case 'user':
220                 filter.valueType = 'user';
221                 break;
222             case 'boolean': 
223                 filter.valueType = 'bool';
224                 filter.defaultValue = false;
225                 break;
226             case 'record':
227                 filterconfig.options.modelName = filterconfig.options.modelName.replace(/_/, '');
228                 var foreignApp = filterconfig.options.appName;
229                 var foreignModel = filterconfig.options.modelName;
230                 
231                 // create generic foreign id filter
232                 var filterclass = Ext.extend(Tine.widgets.grid.ForeignRecordFilter, {
233                     foreignRecordClass: foreignApp + '.' + foreignModel,
234                     linkType: 'foreignId',
235                     ownField: fieldconfig.key,
236                     label: filter.label
237                 });
238                 // register foreign id field as appName.modelName.fieldKey
239                 var fc = appName + '.' + modelName + '.' + fieldconfig.key;
240                 Tine.widgets.grid.FilterToolbar.FILTERS[fc] = filterclass;
241                 filter = {filtertype: fc};
242                 break;
243             case 'tag': 
244                 filter = {filtertype: 'tinebase.tag', app: appName};
245                 break;
246             case 'container':
247                 var applicationName = filterconfig.appName ? filterconfig.appName : appName;
248                 var modelName = filterconfig.modelName ? filterconfig.modelName : modelName;
249                 filter = {
250                     filtertype: 'tine.widget.container.filtermodel', 
251                     app: applicationName, 
252                     recordClass: applicationName + '.' + modelName,
253                     field: fieldconfig.key,
254                     label: fieldconfig.label,
255                     callingApp: appName
256                 };
257                 break;
258             case 'keyfield':
259                 filter.filtertype = 'tine.widget.keyfield.filter';
260                 filter.app = {name: appName};
261                 filter.keyfieldName = fieldconfig.name;
262                 break;
263             case 'date':
264                 filter.valueType = 'date';
265                 break;
266             case 'datetime':
267                 filter.valueType = 'date';
268                 break;
269             case 'float':
270             case 'integer':
271                 filter.valueType = 'number';
272         }
273         return filter;
274     },
275     
276     /**
277      * returns filter
278      * 
279      * @param {String} fieldKey
280      * @param {Object} filterconfig
281      * @param {Object} fieldconfig
282      * @return {Object}
283      */
284     getFilter: function(fieldKey, filterconfig, modelConfig) {
285         // take field label if no filterlabel is defined
286         // TODO Refactor: tag and tags see ticket 0008944
287         // TODO Remove this ugly hack!
288         if (fieldKey == 'tag') {
289             fieldKey = 'tags';
290         }
291         var fieldconfig = modelConfig.fields[fieldKey];
292
293         if (fieldconfig && fieldconfig.type === 'virtual') {
294             fieldconfig = fieldconfig.config;
295         }
296
297         var appName = modelConfig.appName;
298         var modelName = modelConfig.modelName;
299         
300         var app = Tine.Tinebase.appMgr.get(appName);
301         if (! app) {
302             Tine.log.error('Application ' + appName + ' not found!');
303             return null;
304         }
305         
306         // check right on foreign app
307         if (fieldconfig && (fieldconfig.type == 'record' || fieldconfig.type == 'records')) {
308             var opt = fieldconfig.config;
309             
310             if (opt && (! opt.doNotCheckModuleRight) && (! Tine.Tinebase.common.hasRight('view', opt.appName, opt.modelName.toLowerCase()))) {
311                 return null;
312             }
313         }
314         
315         var fieldTypeKey = (fieldconfig && fieldconfig.type) ? fieldconfig.type : (filterconfig && filterconfig.type) ? filterconfig.type : 'default',
316             label = (filterconfig && filterconfig.hasOwnProperty('label')) ? filterconfig.label : (fieldconfig && fieldconfig.hasOwnProperty('label')) ? fieldconfig.label : null,
317             globalI18n = ((filterconfig && filterconfig.hasOwnProperty('useGlobalTranslation')) || (fieldconfig && fieldconfig.hasOwnProperty('useGlobalTranslation')));
318         
319         if (! label) {
320             return null;
321         }
322         // prepare filter
323         var filter = {
324             label: globalI18n ? i18n._(label) : app.i18n._(label),
325             field: fieldKey
326         };
327         
328         if (filterconfig) {
329             if (filterconfig.hasOwnProperty('options') && (filterconfig.options.hasOwnProperty('jsFilterType') || filterconfig.options.hasOwnProperty('jsFilterValueType'))) {
330                 Tine.log.error('jsFilterType and jsFilterValueType are deprecated. Use jsConfig.<property> instead.');
331             }
332             // if js filter is defined in filterconfig.options, take this and return
333             if (filterconfig.hasOwnProperty('jsConfig')) {
334                 Ext.apply(filter, filterconfig.jsConfig);
335                 return filter;
336             } 
337             
338             try {
339                 filter = this.filterMap(fieldTypeKey, fieldconfig, filter, filterconfig, appName, modelName, modelConfig);
340             } catch (e) {
341                 var keys = filterconfig.filter.split('_'),
342                     filterkey = keys[0].toLowerCase() + '.' + keys[2].toLowerCase();
343                     filterkey = filterkey.replace(/filter/g, '');
344     
345                 if (Tine.widgets.grid.FilterToolbar.FILTERS[filterkey]) {
346                     filter = {filtertype: filterkey};
347                 } else { // set to null if no filter could be found
348                     filter = null;
349                 }
350             }
351         }
352
353         return filter;
354     },
355     
356     /**
357      * if application starter should be used, here the js contents are (pre-)created
358      */
359     createStructure: function(initial) {
360         var start = new Date();
361         Ext.each(this.userApplications, function(app) {
362             
363             var appName = app.name;
364             Tine.log.info('ApplicationStarter::createStructure for app ' + appName);
365             Ext.namespace('Tine.' + appName);
366             
367             var models = Tine[appName].registry ? Tine[appName].registry.get('models') : null;
368             
369             if (models) {
370                 
371                 Tine[appName].isAuto = true;
372                 var contentTypes = [];
373                 
374                 // create translation
375                 Tine[appName].i18n = new Locale.Gettext();
376                 Tine[appName].i18n.textdomain(appName);
377                 
378                 // iterate models of this app
379                 Ext.iterate(models, function(modelName, modelConfig) {
380                     // create main screen
381                     if(! Tine[appName].hasOwnProperty('MainScreen')) {
382                         Tine[appName].MainScreen = Ext.extend(Tine.widgets.MainScreen, {
383                             app: appName,
384                             contentTypes: contentTypes,
385                             activeContentType: modelName
386                         });
387                     }
388
389                     var containerProperty = modelConfig.hasOwnProperty('containerProperty') ? modelConfig.containerProperty : null;
390                     
391                     modelName = modelName.replace(/_/, '');
392                     
393                     Ext.namespace('Tine.' + appName, 'Tine.' + appName + '.Model');
394                     
395                     var modelArrayName = modelName + 'Array',
396                         modelArray = [];
397                     
398                     Tine.log.info('ApplicationStarter::createStructure for model ' + modelName);
399                     
400                     if (modelConfig.createModule) {
401                         contentTypes.push(modelConfig);
402                     }
403                     
404                     // iterate record fields
405                     Ext.each(modelConfig.fieldKeys, function(key) {
406                         var fieldDefinition = modelConfig.fields[key];
407
408                         if (fieldDefinition.type === 'virtual') {
409                             fieldDefinition = fieldDefinition.config;
410                         }
411
412                         // add field to model array
413                         modelArray.push(this.getField(fieldDefinition, key));
414
415                         if (fieldDefinition.label) {
416                             // register grid renderer
417                             if (initial) {
418                                 var renderer = null;
419                                 try {
420                                     renderer = this.getGridRenderer(fieldDefinition, key, appName, modelName);
421                                 } catch (e) {
422                                     Tine.log.err(e);
423                                     renderer = null;
424                                 }
425                                 
426                                 if (Ext.isFunction(renderer)) {
427                                     if (! Tine.widgets.grid.RendererManager.has(appName, modelName, key)) {
428                                         Tine.widgets.grid.RendererManager.register(appName, modelName, key, renderer);
429                                     }
430                                 } else if (Ext.isObject(renderer)) {
431                                     if (! Tine.widgets.grid.RendererManager.has(appName, modelName, key)) {
432                                         Tine.widgets.grid.RendererManager.register(appName, modelName, key, renderer.render, null, renderer);
433                                     }
434                                 }
435                             }
436                         }
437                         
438                     }, this);
439                     
440                     // iterate virtual record fields
441                     if (modelConfig.virtualFields && modelConfig.virtualFields.length) {
442                         Ext.each(modelConfig.virtualFields, function(field) {
443                             modelArray.push(this.getField(field, field.key));
444                         }, this);
445                     }
446                     
447                     // collect the filterModel
448                     var filterModel = [];
449                     Ext.iterate(modelConfig.filterModel, function(key, filter) {
450                         var f = this.getFilter(key, filter, modelConfig);
451                         
452                         if (f) {
453                             Tine.widgets.grid.FilterRegistry.register(appName, modelName, f);
454                         }
455                     }, this);
456                     
457                     // TODO: registry loses info if gridpanel resides in an editDialog
458                     // delete filterModel as all filters are in the filter registry now
459                     // delete modelConfig.filterModel;
460                     
461                     Tine[appName].Model[modelArrayName] = modelArray;
462                     
463                     // create model
464                     if (! Tine[appName].Model.hasOwnProperty(modelName)) {
465                         Tine[appName].Model[modelName] = Tine.Tinebase.data.Record.create(Tine[appName].Model[modelArrayName], 
466                             Ext.copyTo({modelConfiguration: modelConfig}, modelConfig,
467                                'idProperty,defaultFilter,appName,modelName,recordName,recordsName,titleProperty,containerProperty,containerName,containersName,group,copyOmitFields')
468                         );
469                         Tine[appName].Model[modelName].getFilterModel = function() {
470                             return filterModel;
471                         }
472                     }
473                     
474                     Ext.namespace('Tine.' + appName);
475                     
476                     // create recordProxy
477                     var recordProxyName = modelName.toLowerCase() + 'Backend';
478                     if (! Tine[appName].hasOwnProperty(recordProxyName)) {
479                         Tine[appName][recordProxyName] = new Tine.Tinebase.data.RecordProxy({
480                             appName: appName,
481                             modelName: modelName,
482                             recordClass: Tine[appName].Model[modelName]
483                         });
484                     }
485                     // if default data is empty, it will be resolved to an array
486                     if (Ext.isArray(modelConfig.defaultData)) {
487                         modelConfig.defaultData = {};
488                     }
489                     
490                     // overwrite function
491                     Tine[appName].Model[modelName].getDefaultData = function() {
492                         if (! dd) {
493                             var dd = Ext.decode(Ext.encode(modelConfig.defaultData));
494                         }
495                         
496                         // find container by selection or use defaultContainer by registry
497                         if (modelConfig.containerProperty) {
498                             if (! dd.hasOwnProperty(modelConfig.containerProperty)) {
499                                 var app = Tine.Tinebase.appMgr.get(appName),
500                                     registry = app.getRegistry(),
501                                     ctp = app.getMainScreen().getWestPanel().getContainerTreePanel();
502                                     
503                                 var container = (ctp ? ctp.getDefaultContainer() : null) || (registry ? registry.get("default" + modelName + "Container") : null);
504                                 
505                                 if (container) {
506                                     dd[modelConfig.containerProperty] = container;
507                                 }
508                             }
509                         }
510                         return dd;
511                     };
512
513                     // create filter panel
514                     var filterPanelName = modelName + 'FilterPanel';
515                     if (! Tine[appName].hasOwnProperty(filterPanelName)) {
516                         Tine[appName][filterPanelName] = function(c) {
517                             Ext.apply(this, c);
518                             Tine[appName][filterPanelName].superclass.constructor.call(this);
519                         };
520                         Ext.extend(Tine[appName][filterPanelName], Tine.widgets.persistentfilter.PickerPanel);
521                     }
522                     // create container tree panel, if needed
523                     if (containerProperty) {
524                         var containerTreePanelName = modelName + 'TreePanel';
525                         if (! Tine[appName].hasOwnProperty(containerTreePanelName)) {
526                             Tine[appName][containerTreePanelName] = Ext.extend(Tine.widgets.container.TreePanel, {
527                                 filterMode: 'filterToolbar',
528                                 recordClass: Tine[appName].Model[modelName]
529                             });
530                         }
531                     }
532                     
533                     // create editDialog openWindow function only if edit dialog exists
534                     var editDialogName = modelName + 'EditDialog';
535                     if (! Tine[appName].hasOwnProperty(editDialogName)) {
536                         Tine[appName][editDialogName] = Ext.extend(Tine.widgets.dialog.EditDialog, {
537                             displayNotes: Tine[appName].Model[modelName].hasField('notes')
538                         });
539                     }
540
541                     
542                     if (Tine[appName].hasOwnProperty(editDialogName)) {
543                         var edp = Tine[appName][editDialogName].prototype;
544                         if (containerProperty) {
545                             edp.showContainerSelector = true;
546                         }
547                         Ext.apply(edp, {
548                             modelConfig:      Ext.encode(modelConfig),
549                             modelName:        modelName,
550                             recordClass:      Tine[appName].Model[modelName],
551                             recordProxy:      Tine[appName][recordProxyName],
552                             appName:          appName,
553                             windowNamePrefix: modelName + 'EditWindow_'
554                         });
555                         if (! Ext.isFunction(Tine[appName][editDialogName].openWindow)) {
556                             Tine[appName][editDialogName].openWindow  = function (cfg) {
557                                 var id = cfg.recordId ? cfg.recordId : ( (cfg.record && cfg.record.id) ? cfg.record.id : 0 );
558                                 var window = Tine.WindowFactory.getWindow({
559                                     width: edp.windowWidth ? edp.windowWidth : 600,
560                                     height: edp.windowHeight ? edp.windowHeight : 230,
561                                     name: edp.windowNamePrefix + id,
562                                     contentPanelConstructor: 'Tine.' + appName + '.' + editDialogName,
563                                     contentPanelConstructorConfig: cfg
564                                 });
565                                 return window;
566                             };
567                         }
568                     }
569                     // create Gridpanel
570                     var gridPanelName = modelName + 'GridPanel', 
571                         gpConfig = {
572                             modelConfig: modelConfig,
573                             app: Tine.Tinebase.appMgr.get(appName),
574                             recordProxy: Tine[appName][recordProxyName],
575                             recordClass: Tine[appName].Model[modelName]
576                         };
577                         
578                     if (! Tine[appName].hasOwnProperty(gridPanelName)) {
579                         Tine[appName][gridPanelName] = Ext.extend(Tine.widgets.grid.GridPanel, gpConfig);
580                     } else {
581                         Ext.apply(Tine[appName][gridPanelName].prototype, gpConfig);
582                     }
583
584                     if (! Tine[appName][gridPanelName].prototype.detailsPanel) {
585                         Tine[appName][gridPanelName].prototype.detailsPanel = {
586                             xtype: 'widget-detailspanel',
587                             recordClass: Tine[appName].Model[modelName]
588                         }
589                     }
590                     // add model to global add splitbutton if set
591                     if (modelConfig.hasOwnProperty('splitButton') && modelConfig.splitButton == true) {
592                         var iconCls = appName + modelName;
593                         if (! Ext.util.CSS.getRule('.' + iconCls)) {
594                             iconCls = 'ApplicationIconCls';
595                         }
596                         Ext.ux.ItemRegistry.registerItem('Tine.widgets.grid.GridPanel.addButton', {
597                             text: Tine[appName].i18n._('New ' + modelName), 
598                             iconCls: iconCls,
599                             scope: Tine.Tinebase.appMgr.get(appName),
600                             handler: (function() {
601                                 var ms = this.getMainScreen(),
602                                     cp = ms.getCenterPanel(modelName);
603                                     
604                                 cp.onEditInNewWindow.call(cp, {});
605                             }).createDelegate(Tine.Tinebase.appMgr.get(appName))
606                         });
607                     }
608                     
609                 }, this);
610             }
611         }, this);
612         
613         var stop = new Date();
614     }
615 }