912a872f90ffbdabfe7486240e296d2574be4730
[tine20] / tine20 / Tinebase / js / widgets / relation / GenericPickerGridPanel.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-2014 Metaways Infosystems GmbH (http://www.metaways.de)
8  *
9  */
10 Ext.ns('Tine.widgets.relation');
11 /**
12  * @namespace   Tine.widgets.relation
13  * @class       Tine.widgets.relation.GenericPickerGridPanel
14  * @extends     Tine.widgets.grid.PickerGridPanel
15  * @author      Alexander Stintzing <a.stintzing@metaways.de>
16  */
17 Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.PickerGridPanel, {
18     /**
19      * @cfg for PickerGridPanel
20      */
21     recordClass: Tine.Tinebase.Model.Relation,
22     clicksToEdit: 1,
23     selectRowAfterAdd: false,
24     
25     /**
26      * disable Toolbar creation, create a custom one
27      * @type Boolean
28      */
29     enableTbar: false,
30     title: null,
31     /**
32      * the record
33      * @type Record
34      */
35     record: null,
36
37     /**
38      *
39      * @type {String}
40      */
41     ownRecordClass: null,
42     /**
43      * @type Tinebase.Application
44      */
45     app: null,
46     /**
47      * The calling EditDialog
48      * @type Tine.widgets.dialog.EditDialog
49      */
50     editDialog: null,
51     /**
52      * reference to relationPanelRegistry from the editdialog
53      * 
54      * @type {Array}
55      */
56     relationPanelRegistry: null,
57     /* private */
58     /**
59      * configuration fetched from registry
60      * @type {Object}
61      * autoconfig
62      */
63     possibleRelations: null,
64     /**
65      * Selects the Model to relate to
66      * @type {Ext.form.ComboBox} modelCombo
67      */
68     modelCombo: null,
69     /**
70      * Array with searchcombos for each model
71      * @type {Array}
72      */
73     searchCombos: null,
74     /**
75      * The by the modelcombo activated model
76      * @type {String}
77      */
78     activeModel: null,
79     /**
80      * Array of possible degrees
81      * @type {Array} degreeData
82      */
83     degreeData: null,
84     /**
85      * keyfieldConfigs shortcuts
86      * @type {Object} keyFieldConfigs
87      */
88     keyFieldConfigs: null,
89     /**
90      * constrains config
91      * @type {Object} constraintsConfig
92      */
93     constraintsConfig: null,
94
95     /**
96      * @cfg {String} requiredGrant to make actions
97      */
98     requiredGrant: 'editGrant',
99
100     /* config */
101     frame: true,
102     border: true,
103     autoScroll: true,
104     layout: 'fit',
105     stateful: true,
106     stateId: 'widgets-relation-genereic-picker-grid',
107     canonicalName: 'RelationsGrid',
108
109     /**
110      * initializes the component
111      */
112     initComponent: function() {
113         this.record = this.editDialog.record;
114         this.app = this.editDialog.app;
115         this.ownRecordClass = this.editDialog.recordClass;
116         
117         this.ignoreRelatedModels = this.editDialog.ignoreRelatedModels ? [].concat(this.editDialog.ignoreRelatedModels) : [];
118         this.relationPanelRegistry = this.editDialog.relationPanelRegistry;
119         
120         if (this.relationPanelRegistry && this.relationPanelRegistry.length) {
121             Ext.each(this.relationPanelRegistry, function(panel) {
122                 if (panel.relatedPhpModel) {
123                     this.ignoreRelatedModels.push(panel.relatedPhpModel);
124                 }
125             }, this);
126         }
127         
128         this.possibleRelations = Tine.widgets.relation.Manager.get(this.app, this.ownRecordClass, this.ignoreRelatedModels);
129         
130         this.initTbar();
131         this.viewConfig = {
132             getRowClass: this.getViewRowClass,
133             enableRowBody: true
134             };
135         this.actionEditInNewWindow = new Ext.Action({
136             text: i18n._('Edit record'),
137             disabled: true,
138             scope: this,
139             handler: this.onEditInNewWindow,
140             iconCls: 'action_edit'
141         });
142         
143         this.title = this.i18nTitle = i18n.ngettext('Relation', 'Relations', 50);
144         
145         //Tine.widgets.dialog.MultipleEditDialogPlugin.prototype.registerSkipItem(this);
146
147         this.on('rowdblclick', this.onEditInNewWindow.createDelegate(this), this);
148         
149         this.on('beforecontextmenu', this.onBeforeContextMenu.createDelegate(this), this);
150         
151         this.contextMenuItems = [this.actionEditInNewWindow];
152         // preparing keyfield and constrains configs
153         this.keyFieldConfigs = {};
154         this.constraintsConfig = {};
155         
156         Ext.each(this.app.getRegistry().get('relatableModels'), function(rel) {
157             if (rel.ownModel == this.ownRecordClass.getMeta('modelName')) {
158                 if (rel.keyfieldConfig) {
159                     if (rel.keyfieldConfig.from == 'foreign') {
160                         this.keyFieldConfigs[rel.relatedApp + rel.relatedModel] = {app: rel.relatedApp, name: rel.keyfieldConfig.name};
161                     } else {
162                         this.keyFieldConfigs[this.app.name + rel.ownModel] = {app: this.app.name, name: rel.keyfieldConfig.name};
163                     }
164                 }
165                 if (rel.config) {
166                     this.constraintsConfig[rel.relatedApp + rel.relatedModel] = rel.config;
167                 }
168             }
169         }, this);
170
171         this.degreeData = [
172             ['sibling', i18n._('Sibling')],
173             ['parent', i18n._('Parent')],
174             ['child', i18n._('Child')]
175         ];
176         
177         this.on('beforeedit', this.onBeforeRowEdit, this);
178         this.on('validateedit', this.onValidateRowEdit, this);
179         this.on('afteredit', this.onAfterRowEdit, this);
180         
181         this.editDialog.on('recordUpdate', this.onRecordUpdate, this);
182         this.editDialog.on('load', this.loadRecord, this);
183
184         Tine.widgets.relation.GenericPickerGridPanel.superclass.initComponent.call(this);
185
186         this.selModel.on('selectionchange', function(sm) {
187             this.actionEditInNewWindow.setDisabled(sm.getCount() != 1);
188         }, this);
189         
190         this.store.on('add', this.onAdd, this);
191     },
192     
193     /**
194      * is called from onApplyChanges of the edit dialog per save event
195      * 
196      * @param {Tine.widgets.dialog.EditDialog} dialog
197      * @param {Tine.Tinebase.data.Record} record
198      * @return {Boolean}
199      */
200     onRecordUpdate: function(dialog, record) {
201         // update from relationsPanel if any
202         if (this.isValid()) {
203             if (record.data.hasOwnProperty('relations')) {
204                 record.data.relations = null;
205                 delete record.data.relations;
206             }
207             var relations = [];
208             
209             Ext.each(this.relationPanelRegistry, function(panel) {
210                 relations = relations.concat(this.getData(panel.store));
211             }, this);
212             
213             relations = relations.concat(this.getData());
214             
215             record.set('relations', relations);
216         } else {
217             this.editDialog.loadMask.hide();
218             this.editDialog.saving = false;
219             return false;
220         }
221     },
222     
223     /**
224      * updates the title ot the tab
225      * @param {Number} count
226      */
227     updateTitle: function(count) {
228         if (! Ext.isNumber(count)) {
229             count = 0;
230             this.store.each(function(record){
231                 if (this.ignoreRelatedModels.indexOf(record.get('related_model')) == -1) {
232                     count++;
233                 }
234             }, this);
235         }
236         
237         this.setTitle(this.i18nTitle + ' (' + count + ')');
238     },
239     
240     /**
241      * creates the toolbar
242      */
243     initTbar: function() {
244         var items = [this.getModelCombo(), ' '];
245         items = items.concat(this.createSearchCombos());
246
247         this.tbar = new Ext.Toolbar({
248             items: items
249         });
250     },
251
252     /**
253      * adds invalid row class to a invalid row and adds the error qtip
254      * 
255      * @param {Tine.Tinebase.data.Record} record
256      * @param {Integer} index
257      * @param {Object} rowParams
258      * @param {Ext.data.store} store
259      * @scope this.view
260      * @return {String}
261      */
262     getViewRowClass: function(record, index, rowParams, store) {
263         var relatedModel = record.get('related_model').split('_Model_');
264             relatedModel = Tine[relatedModel[0]].Model[relatedModel[1]];
265         
266         var ownModel = record.get('own_model').split('_Model_');
267             ownModel = Tine[ownModel[0]].Model[ownModel[1]];
268             
269         if (this.invalidRowRecords && this.invalidRowRecords.indexOf(record.id) !== -1) {
270             rowParams.body = '<div style="height: 19px; margin-top: -19px" ext:qtip="' +
271                 String.format(i18n._("The maximum number of {0} with the type \"{1}\" is reached. Please change the type of this relation"), ownModel.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record))
272                 + '"></div>';
273             return 'tine-editorgrid-row-invalid';
274         } else if (this.invalidRelatedRecords && this.invalidRelatedRecords.indexOf(record.id) !== -1) {
275             rowParams.body = '<div style="height: 19px; margin-top: -19px" ext:qtip="' +
276                 String.format(i18n._("The maximum number of {0}s with the type \"{1}\" is reached at the {2} you added. Please change the type of this relation or edit the {2}"), ownModel.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record), relatedModel.getRecordName())
277                 + '"></div>';
278             return 'tine-editorgrid-row-invalid';
279         }
280         
281         rowParams.body='';
282         return '';
283     },
284     
285     /**
286      * calls the editdialog for the model
287      */
288     onEditInNewWindow: function() {
289         var selected = this.getSelectionModel().getSelected(),
290             app = selected.get('related_model').split('_Model_')[0],
291             model = selected.get('related_model').split('_Model_')[1],
292             ms = Tine.Tinebase.appMgr.get(app).getMainScreen(),
293             recordData = selected.get('related_record'),
294             record = new Tine[app].Model[model](recordData),
295             cp = ms.getCenterPanel(model);
296         
297         if (Ext.isFunction(cp.onEditInNewWindow)) {
298             cp.onEditInNewWindow({actionType: 'edit', mode: 'remote'}, record);
299         } else {
300             Ext.MessageBox.show({
301                 buttons: Ext.Msg.OK,
302                 icon: Ext.MessageBox.WARNING,
303                 title: i18n._('No Dialog'),
304                 msg: i18n._("The Record can't be opened. There doesn't exist any dialog for editing this Record!")
305             });
306         }
307     },
308     
309     /**
310      * is called before context menu is shown
311      * adds additional menu items from Tine.widgets.relation.MenuItemManager
312      * @param {Tine.widgets.grid.PickerGridPanel} grid
313      * @param {Integer} index
314      * @return {Boolean}
315      */
316     onBeforeContextMenu: function(grid, index) {
317         var record = grid.store.getAt(index),
318             rm = record.get('related_model');
319             if(!rm) {
320                 return;
321             }
322         var model = rm.split('_Model_');
323         
324         var app = Tine.Tinebase.appMgr.get(model[0]);
325         var additionalItems = Tine.widgets.relation.MenuItemManager.get(model[0], model[1], {
326             scope: app,
327             grid: grid,
328             gridIndex: index
329         });
330         
331         Ext.each(additionalItems, function(item) {
332             item.setText(app.i18n._(item.getText()));
333             this.contextMenu.add(item);
334             if(! this.contextMenu.hasOwnProperty('tempItems')) {
335                 this.contextMenu.tempItems = [];
336             }
337             this.contextMenu.tempItems.push(item);
338         }, this);
339     },
340     
341     /**
342      * creates the model combo
343      * @return {Ext.form.ComboBox}
344      */
345     getModelCombo: function() {
346         if (!this.modelCombo) {
347             var data = [];
348             var id = 0;
349
350             Ext.each(this.possibleRelations, function(rel) {
351                 data.push([id, rel.text, rel.relatedApp, rel.relatedModel]);
352                 id++;
353             }, this);
354
355             this.modelCombo = new Ext.form.ComboBox({
356                 store: new Ext.data.ArrayStore({
357                     fields: ['id', 'text', 'appName', 'modelName'],
358                     data: data
359                 }),
360
361                 allowBlank: false,
362                 forceSelection: true,
363                 value: data.length > 0 ? data[0][0] : null,
364                 displayField: 'text',
365                 valueField: 'id',
366                 idIndex: 0,
367                 mode: 'local',
368                 triggerAction: 'all',
369                 selectOnFocus: true,
370                 listeners: {
371                     scope: this,
372                     select: this.onModelComboSelect
373                 }
374             });
375
376         }
377         return this.modelCombo;
378     },
379     
380     /**
381      * is called on change listener of this.modelCombo
382      * 
383      * @param {Ext.form.ComboBox} combo the calling combo
384      * @param {Ext.data.record} selected the selected record
385      * @param {Number} index the selection index
386      */
387     onModelComboSelect: function(combo, selected, index) {
388         this.showSearchCombo(selected.get('appName'), selected.get('modelName'));
389     },
390     
391     /**
392      * creates the searchcombos for the models
393      * @return {Tine.Tinebase.widgets.form.RecordPickerComboBox}
394      */
395     createSearchCombos: function() {
396         var sc = [];
397         this.searchCombos = {};
398
399         Ext.each(this.possibleRelations, function(rel) {
400             var key = rel.relatedApp+rel.relatedModel;
401             this.searchCombos[key] = Tine.widgets.form.RecordPickerManager.get(rel.relatedApp, rel.relatedModel,{
402                 width: 300,
403                 allowBlank: true,
404                 listeners: {
405                     scope: this,
406                     select: this.onAddRecordFromCombo
407                 }
408             });
409             sc.push(this.searchCombos[key]);
410             this.searchCombos[key].hide();
411         }, this);
412
413         this.showSearchCombo(this.possibleRelations[0].relatedApp, this.possibleRelations[0].relatedModel);
414         return sc;
415     },
416     
417     /**
418      * shows the active model searchcombo
419      * @param {String} appName
420      * @param {String} modelName
421      */
422     showSearchCombo: function(appName, modelName) {
423         var key = appName+modelName;
424         if(this.activeModel) this.searchCombos[this.activeModel].hide();
425         this.searchCombos[key].show();
426         this.activeModel = appName+modelName;
427     },
428     /**
429      * returns the active search combo
430      * @return {}
431      */
432     getActiveSearchCombo: function() {
433         return this.searchCombos[this.activeModel];
434     },
435
436     /**
437      * @return Ext.grid.ColumnModel
438      * @private
439      */
440     getColumnModel: function () {
441
442         if (! this.colModel) {
443
444             this.degreeEditor = new Ext.form.ComboBox({
445                 store: new Ext.data.ArrayStore({
446                     fields: ['id', 'value'],
447                     data: this.degreeData
448                 }),
449                 allowBlank: false,
450                 displayField: 'value',
451                 valueField: 'id',
452                 mode: 'local'
453             });
454
455             this.colModel = new Ext.grid.ColumnModel({
456                 defaults: {
457                     sortable: true,
458                     width: 180
459                 },
460                 columns: [
461                     {id: 'related_model', dataIndex: 'related_model', header: i18n._('Record'), editor: false, renderer: this.relatedModelRenderer.createDelegate(this), scope: this},
462                     {id: 'related_record', dataIndex: 'related_record', header: i18n._('Description'), renderer: this.relatedRecordRenderer.createDelegate(this), editor: false, scope: this},
463                     {id: 'remark', dataIndex: 'remark', header: i18n._('Remark'), renderer: this.remarkRenderer.createDelegate(this), editor: Ext.form.Field, scope: this, width: 120},
464                     {id: 'related_degree', hidden: true, dataIndex: 'related_degree', header: i18n._('Dependency'), editor: this.degreeEditor, renderer: this.degreeRenderer.createDelegate(this), scope: this, width: 100},
465                     {id: 'type', dataIndex: 'type', renderer: this.typeRenderer, header: i18n._('Type'),  scope: this, width: 120, editor: true},
466                     {id: 'creation_time', dataIndex: 'creation_time', editor: false, renderer: Tine.Tinebase.common.dateTimeRenderer, header: i18n._('Creation Time'), width: 140}
467                 ]
468             });
469         }
470         return this.colModel;
471     },
472     /**
473      * creates the special editors
474      * @param {} o
475      */
476     onBeforeRowEdit: function(o) {
477         var model = o.record.get('related_model').split('_Model_');
478         var app = model[0];
479         model = model[1];
480         var colModel = o.grid.getColumnModel();
481
482         // create editor if values are defined in the constraints config
483         switch (o.field) {
484             case 'type':
485                 var editor = null;
486                 if (this.constraintsConfig[app+model]) {
487                     editor = this.getTypeEditor(this.constraintsConfig[app+model], this.app);
488                 } else if (this.keyFieldConfigs[app+model]) {
489                     editor = new Tine.Tinebase.widgets.keyfield.ComboBox({
490                         app: app,
491                         keyFieldName: this.keyFieldConfigs[app+model].name
492                     });
493                 }
494                 if (editor) {
495                     colModel.config[o.column].setEditor(editor);
496                 } else {
497                     colModel.config[o.column].setEditor(null);
498                 }
499                 break;
500             default: return;
501         }
502         
503         if (colModel.config[o.column].editor) {
504             colModel.config[o.column].editor.selectedRecord = null;
505         }
506     },
507
508     /**
509      * returns the type editors for each row in the grid
510      * 
511      * @param {Object} config
512      * @param {Tine.Tinebase.Application}
513      * 
514      * @return {}
515      */
516     getTypeEditor: function(config, app) {
517         var data = [['', '']];
518         Ext.each(config, function(c){
519             data.push([c.type.toUpperCase(), app.i18n._hidden(c.text)]);
520         });
521         
522         return new Ext.form.ComboBox({
523             store: new Ext.data.ArrayStore({
524                 fields: ['id', 'value'],
525                 data: data
526             }),
527             allowBlank: true,
528             displayField: 'value',
529             valueField: 'id',
530             mode: 'local',
531             constraintsConfig: config,
532             listeners: {
533                 scope: this,
534                 select: this.onTypeChange
535             },
536             tpl: '<tpl for="."><div class="x-combo-list-item">{value}&nbsp;</div></tpl>'
537         });
538     },
539
540     /**
541      * is called on type change, sets the relationpickercombos accordingly
542      * 
543      * @param {Ext.form.ComboBox} combo
544      * @param {Tine.Tinebase.data.Record} record
545      * @param {Number} index
546      */
547     onTypeChange: function(combo, record, index) {
548         
549         var newType = combo.getValue();
550         var oldType = combo.startValue;
551         
552         var rp = this.editDialog.relationPickers;
553         if (rp && rp.length) {
554             var relatedRecord;
555             Ext.each(rp, function(picker) {
556                 // remove record from old combo
557                 if (picker.relationType == oldType) {
558                     relatedRecord = picker.combo.selectedRecord;
559                     picker.clear();
560                 }
561             }, this);
562             
563             Ext.each(rp, function(picker) {
564                 if (picker.relationType == newType) {
565                     picker.setValue(relatedRecord);
566                 }
567             }, this);
568         }
569     },
570     
571     /**
572      * related record renderer
573      *
574      * @param {Record} value
575      * @return {String}
576      */
577     relatedRecordRenderer: function (recData, meta, relRec) {
578         var relm = relRec.get('related_model');
579         if (! relm) {
580             return '';
581         }
582         var split = relm.split('_Model_'); 
583         var recordClass = Tine[split[0]].Model[split[1]];
584         var record = new recordClass(recData);
585         var result = '';
586         if (recData) {
587             result = Ext.util.Format.htmlEncode(record.getTitle());
588         }
589         return result;
590     },
591     
592     /**
593      * renders the remark
594      * @param {String} value
595      * @param {Object} row
596      * @param {Tine.Tinebase.data.Record} record
597      * @return {String}
598      */
599     remarkRenderer: function(value, row, record) {
600         if (record && record.get('related_model')) {
601             var app = Tine.Tinebase.appMgr.get(record.get('related_model').split('_Model_')[0]);
602         }
603         if (! value) {
604             value = '';
605         } else if (Ext.isObject(value)) {
606             var str = '';
607             Ext.iterate(value, function(label, val) {
608                 str += app.i18n._(Ext.util.Format.capitalize(label)) + ': ' + (Ext.isNumber(val) ? val : Ext.util.Format.capitalize(val)) + ', ';
609             }, this);
610             value = str.replace(/, $/,'');
611         } else if (Ext.isArray(value)) {
612             var str = '';
613             Ext.each(value, function(val) {
614                 str += (Ext.isNumber(val) ? val : Ext.util.Format.capitalize(val)) + ', ';
615             }, this);
616             value = str.replace(/, $/,'');
617         } else if(value.match(/^\[.*\]$/)) {
618             value = Ext.decode(value);
619             return this.remarkRenderer(value);
620         }
621         
622         return Ext.util.Format.htmlEncode(value);
623     },
624     /**
625      * renders the degree
626      * @param {String} value
627      * @return {String}
628      */
629     degreeRenderer: function(value) {
630         if(!this.degreeDataObject) {
631             this.degreeDataObject = {};
632             Ext.each(this.degreeData, function(dd) {
633                 this.degreeDataObject[dd[0]] = dd[1];
634             }, this);
635         }
636         return this.degreeDataObject[value] ? i18n._(this.degreeDataObject[value]) : '';
637     },
638
639     /**
640      * renders the titleProperty of the models
641      * @param {String} value
642      * @return {String}
643      */
644     relatedModelRenderer: function(value) {
645         if(!value) {
646             return '';
647         }
648         var split = value.split('_Model_');
649         var model = Tine[split[0]].Model[split[1]];
650         return '<span class="tine-recordclass-gridicon ' + model.getMeta('appName') + model.getMeta('modelName') + '">&nbsp;</span>' + model.getRecordName() + ' (' + model.getAppName() + ')';
651     },
652
653     /**
654      * renders the type
655      * @param {String} value
656      * @param {Object} row
657      * @param {Tine.Tinebase.data.Record} rec
658      * @return {String}
659      */
660     typeRenderer: function(value, row, rec) {
661         if(! rec.get('own_model') || ! rec.get('related_model')) {
662             return '';
663         }
664         var o = rec.get('own_model').split('_Model_').join('');
665         var f = rec.get('related_model').split('_Model_').join('');
666
667         var renderer = Ext.util.Format.htmlEncode;
668         if (this.constraintsConfig[f]) {
669             Ext.each(this.constraintsConfig[f], function(c){
670                 if(c.type == value) value = this.app.i18n._hidden(c.text);
671             }, this);
672         } else if(this.keyFieldConfigs[o]) {
673             renderer = Tine.Tinebase.widgets.keyfield.Renderer.get(this.keyFieldConfigs[o].app, this.keyFieldConfigs[o].name);
674         } else if(this.keyFieldConfigs[f]) {
675             renderer = Tine.Tinebase.widgets.keyfield.Renderer.get(this.keyFieldConfigs[f].app, this.keyFieldConfigs[f].name);
676         }
677
678         return renderer(value);
679     },
680
681     /**
682      * returns the default relation values
683      * @return {Array}
684      */
685     getRelationDefaults: function() {
686         return {
687             own_backend: 'Sql',
688             related_backend: 'Sql',
689             own_id: (this.record) ? this.record.id : null,
690             own_model: this.ownRecordClass.getPhpClassName()
691         };
692     },
693
694     /**
695      * is called when selecting a record in the searchCombo (relationpickercombo)
696      */
697     onAddRecordFromCombo: function(node) {
698         var record = null;
699
700         if (this.getActiveSearchCombo().hasOwnProperty('store')) {
701             record = this.getActiveSearchCombo().store.getById(this.getActiveSearchCombo().getValue())
702         } else {
703             record = node;
704         }
705
706         if (! record) {
707             return;
708         }
709         
710         if (Ext.isArray(this.constraintsConfig[this.activeModel])) {
711             var relconf = {type: this.constraintsConfig[this.activeModel][0]['type']};
712         } else {
713             var relconf = {};
714         }
715         
716         this.onAddRecord(record, relconf);
717
718         if (Ext.isFunction(this.getActiveSearchCombo().collapse)) {
719             this.getActiveSearchCombo().collapse();
720         }
721         if (Ext.isFunction(this.getActiveSearchCombo().reset)) {
722             this.getActiveSearchCombo().reset();
723         }
724     },
725
726     /**
727      * call to add relation from an external component
728      *
729      *  @todo refactor this trash
730      *
731      * @param {Tine.Tinebase.data.Record} record
732      * @param {Object} relconf
733      */
734     onAddRecord: function(record, relconf) {
735         if (record) {
736             if (! relconf) {
737                 relconf = {};
738             }
739             if (record.data && record.data.hasOwnProperty('relations')) {
740                 record.data.relations = null;
741                 delete record.data.relations;
742             }
743             var rc = this.getActiveSearchCombo().recordClass;
744             var relatedPhpModel = rc.getPhpClassName();
745
746             var app = rc.getMeta('appName'), model = rc.getMeta('modelName'), f = app + model;
747             var type = '';
748
749             if (this.constraintsConfig[f] && this.constraintsConfig[f].length) {
750                 // per default the first defined type is used
751                 var type = this.constraintsConfig[f][0].type;
752             }
753             
754             var rc = this.getActiveSearchCombo().recordClass,
755                 relatedPhpModel = rc.getPhpClassName(),
756                 appName = rc.getMeta('appName'), 
757                 model = rc.getMeta('modelName'), 
758                 f = appName + model,
759                 type = '';
760
761             var relationRecord = new Tine.Tinebase.Model.Relation(Ext.apply(this.getRelationDefaults(), Ext.apply({
762                 related_record: record.data || record,
763                 related_id: record.id,
764                 related_model: relatedPhpModel,
765                 type: type,
766                 related_degree: 'sibling'
767             }, relconf)), Ext.id());
768             
769             var mySideValid = true;
770             
771             if (this.constraintsConfig[f]) {
772                 if (this.constraintsConfig[f].length) {
773                     // per default the first defined type is used
774                     var type = this.constraintsConfig[f][0].type;
775                 }
776                 // validate constrains config from own side
777                 mySideValid = this.checkLocalConstraints(appName, model, relationRecord, type);
778             }
779             
780             // if my side is not valid, it's ok to skip related constraints validation, the relation is marked invalid already
781             if (mySideValid) {
782                 this.validateRelatedConstrainsConfig(record, relationRecord);
783             } else {
784                 this.onAddNewRelationToStore(relationRecord, record);
785             }
786         }
787     },
788     
789     /**
790      * validates the constrains of the related record , fetches the relations
791      * 
792      * @param {Tine.Tinebase.data.Record} record
793      * @param {Tine.Tinebase.Model.Relation} relationRecord
794      */
795     validateRelatedConstrainsConfig: function(record, relationRecord) {
796         var rc = relationRecord.get('related_model').split(/_Model_/);
797         var rc = Tine[rc[0]].Model[rc[1]];
798
799         var appName = rc.getMeta('appName'); 
800         var model = rc.getMeta('modelName'); 
801         var relatedApp = Tine.Tinebase.appMgr.get(appName); 
802         var relatedConstrainsConfig = relatedApp.getRegistry().get('relatableModels');
803         var ownRecordClassName = this.editDialog.recordClass.getMeta('modelName');
804         var relatedRecordProxy = this.getActiveSearchCombo().recordProxy || Tine[appName][(model.toLowerCase() + 'Backend')];
805         
806         if (! Ext.isFunction(record.get)) {
807             record = relatedRecordProxy.recordReader({responseText: Ext.encode(record)});
808         }
809         
810         if (relatedConstrainsConfig) {
811             for (var index = 0; index < relatedConstrainsConfig.length; index++) {
812                 var rcc = relatedConstrainsConfig[index];
813                 
814                 if ((rcc.relatedApp == this.app.name) && (rcc.relatedModel == ownRecordClassName)) {
815                     var myRelatedConstrainsConfig = rcc;
816                     break;
817                 }
818             }
819         }
820         
821         // validate constrains config from other side if a config exists
822         if (myRelatedConstrainsConfig) {
823             // if relations hasn't been fetched already, fetch them now
824             if (! Ext.isArray(record.data.relations) || record.data.relations.length === 0) {
825                 relatedRecordProxy.loadRecord(record, { 
826                     success: function(record) {
827                         // if record has relations, validate each relation
828                         if (Ext.isArray(record.get('relations')) && record.get('relations').length > 0) {
829                             this.onValidateRelatedConstrainsConfig(myRelatedConstrainsConfig.config, relationRecord, appName, model, record);
830                         } else {
831                             // if there aren't any relations, no validation is needed
832                             this.onAddNewRelationToStore(relationRecord, record);
833                         }
834                     },
835                     // don't break on failure, use given record instead
836                     failure: this.onAddNewRelationToStore.createDelegate(this, [relationRecord, record]),
837                     scope: this
838                 });
839             
840             } else {
841                 this.onValidateRelatedConstrainsConfig(myRelatedConstrainsConfig.config, relationRecord, appName, model, record);
842             }
843         } else {
844             this.onAddNewRelationToStore(relationRecord, record);
845         }
846     },
847     
848     /**
849      * validates the constrains of the related record after fetching relations
850      * 
851      * @param {Object} constraintsConfig
852      * @param {Tine.Tinebase.Model.Relation} relationRecord
853      * @param {String} relatedAppName
854      * @param {String} relatedModelName
855      * @param {Tine.Tinebase.data.Record} record
856      */
857     onValidateRelatedConstrainsConfig: function(constraintsConfig, relationRecord, relatedAppName, relatedModelName, record) {
858         
859         var invalid = false;
860         
861         Ext.each(constraintsConfig, function(conf) {
862         
863             if (conf.hasOwnProperty('max') && conf.max > 0 && (conf.type == relationRecord.get('type'))) {
864                 var rr = record.get('relations'),
865                     count = 0;
866                 
867                 for (var index = 0; index < rr.length; index++) {
868                     if (rr[index].type == conf.type) {
869                         count++;
870                     }
871                 }
872                 
873                 if (count >= conf.max) {
874                     if (! this.view) {
875                         this.view = {};
876                     }
877                     if (! this.view.invalidRelatedRecords) {
878                         this.view.invalidRelatedRecords = [];
879                     }
880                     
881                     this.view.invalidRelatedRecords.push(relationRecord.id);
882
883                     invalid = true;
884                 }
885             } 
886         }, this);
887         
888         if (! invalid && Ext.isArray(this.view.invalidRelatedRecords)) {
889             var index = this.view.invalidRelatedRecords.indexOf(record.getId());
890             if (index > -1) {
891                 this.view.invalidRelatedRecords.splice(index, 1);
892             }
893         }
894
895         this.onAddNewRelationToStore(relationRecord, record);
896     },
897     
898     /**
899      * Is called after all validation is done
900      * 
901      * @param {Tine.Tinebase.Model.Relation} relationRecord
902      * @param {Tine.Tinebase.data.Record} record
903      */
904     onAddNewRelationToStore: function(relationRecord, record) {
905         var _ = window.lodash;
906
907         if (_.get(relationRecord, 'data.related_record.relations', false)) {
908             relationRecord.data.related_record.relations = null;
909             delete relationRecord.data.related_record.relations;
910         }
911
912         if (this.relationCheck(relationRecord)) {
913             Tine.log.debug('Adding new relation:');
914             Tine.log.debug(relationRecord);
915             this.store.add([relationRecord]);
916         }
917
918         this.view.refresh();
919     },
920     
921     /**
922      * checks if record to add is already linked or is the same record
923      * @param {Tine.Tinebase.data.Record} recordToAdd
924      * @param {String} relatedModel
925      * @return {Boolean}
926      */
927     relationCheck: function(recordToAdd, relatedModel) {
928         var duplicateIdx = Tine.Tinebase.Model.Relation.findDuplicate(this.store, recordToAdd),
929             duplicateIsSelf = this.store.getAt(duplicateIdx) == recordToAdd,
930             add = !duplicateIsSelf;
931
932         if (duplicateIdx >= 0 && ! duplicateIsSelf) {
933             Ext.MessageBox.show({
934                 title: i18n._('Failure'),
935                 msg: i18n._('The record you tried to link is already linked. Please edit the existing link.'),
936                 buttons: Ext.MessageBox.OK,
937                 icon: Ext.MessageBox.INFO
938             });
939             add = false;
940             return false;
941         }
942
943         // don't allow to relate itself
944         if((this.ownRecordClass.getMeta('phpClassName') == relatedModel) && recordToAdd.getId() == this.editDialog.record.getId()) {
945             Ext.MessageBox.show({
946                 title: i18n._('Failure'),
947                 msg: i18n._('You tried to link a record with itself. This is not allowed!'),
948                 buttons: Ext.MessageBox.OK,
949                 icon: Ext.MessageBox.ERROR  
950             });
951             add = false;
952         }
953         return add;
954     },
955     
956     /**
957      * is called after a row has been edited
958      * @param {Object} o
959      */
960     onAfterRowEdit: function(o) {
961         this.onUpdate(o.grid.store, o.record);
962         this.view.refresh();
963     },
964     
965     /**
966      * validates constrains config, is called after row edit
967      * @param {Object} o
968      */
969     onValidateRowEdit: function(o) {
970         if (o.field === 'type') {
971             
972             var index = -1;
973             
974             this.store.remove(o.record.getId());
975             this.removeFromInvalidRelatedRecords(o.record);
976             this.removeFromInvalidRowRecords(o.record);
977             
978             var model = o.record.get('related_model').split('_Model_');
979             var app = model[0];
980             
981             model = model[1];
982             
983             // check constrains from own_record side
984             if (this.constraintsConfig[app + model]) {
985                 this.checkLocalConstraints(app, model, o.record, o.value, o.originalValue);
986             }
987             
988             // check constrains from other side
989             this.validateRelatedConstrainsConfig(o.record.get('related_record'), o.record, true);
990         }
991         
992         return true;
993     },
994     
995     /**
996      * Checks local relation constraints
997      * 
998      * @param {} app
999      * @param {} model
1000      * @param {} checkRecord
1001      * @param {} type
1002      * @param {} oldType
1003      * 
1004      * @return {Bool}
1005      */
1006     checkLocalConstraints: function(app, model, checkRecord, type, oldType) {
1007         // remove itself at first
1008         
1009         if (! this.view.invalidRowRecords) {
1010             this.view.invalidRowRecords = [];
1011         }
1012             
1013         var retVal = true;
1014         
1015         this.view.invalidRowRecords.remove(checkRecord.get('id'));
1016         Ext.each(this.constraintsConfig[app + model], function(conf) {
1017             // check new value
1018             if (conf.max && conf.max > 0 && (conf.type == type)) {
1019                 var resNew = this.store.queryBy(function(record, id) {
1020                     if ((type == record.get('type')) && (record.get('related_model') == (app + '_Model_' + model))) {
1021                         return true;
1022                     } else {
1023                         return false;
1024                     }
1025                 }, this);
1026                 
1027                 // add all record ids to invalidRecords, if maximum is reached
1028                 if(resNew.getCount() >= conf.max) {
1029                     resNew.each(function(item) {
1030                         if (this.view.invalidRowRecords.indexOf(item.id) === -1) {
1031                             this.view.invalidRowRecords.push(item.id);
1032                             retVal = false;
1033                         }
1034                     }, this);
1035                     if (this.view.invalidRowRecords.indexOf(checkRecord.id) === -1) {
1036                         this.view.invalidRowRecords.push(checkRecord.id);
1037                         retVal = false;
1038                     }
1039                 }
1040             }
1041             
1042             if (oldType) {
1043                 this.checkOldValidations(app, model, checkRecord, oldType);
1044             }
1045         }, this);
1046
1047         return retVal;
1048     },
1049     
1050     /**
1051      * checks existing relation constraints
1052      * 
1053      * @param {} app
1054      * @param {} model
1055      * @param {} checkRecord
1056      * @param {} type
1057      */
1058     checkOldValidations: function(app, model, checkRecord, type) {
1059         // check old value if given
1060         var configs = this.constraintsConfig[app + model];
1061         
1062         if (configs) {
1063             for (var index = 0; index < configs.length; index++) {
1064                 var conf = configs[index];
1065                 if (conf.hasOwnProperty('max') && conf.max > 0 && (conf.type == type)) {
1066                     var resOld = this.store.queryBy(function(record, id) {
1067                         if ((type == record.get('type')) && (record.get('related_model') == (app + '_Model_' + model))) {
1068                             return true;
1069                         } else {
1070                             return false;
1071                         }
1072                     }, this);
1073                     
1074                     if (this.view.hasOwnProperty('invalidRowRecords') && Ext.isArray(this.view.invalidRowRecords)) {
1075                         if ((resOld.getCount() - 1) <= conf.max) {
1076                             resOld.each(function(item) {
1077                                 this.view.invalidRowRecords.remove(item.id);
1078                             }, this);
1079                         }
1080                     }
1081                 }
1082             }
1083         }
1084         
1085         this.view.refresh();
1086     },
1087     
1088     /**
1089      * removes a record from invalid records array
1090      * 
1091      * @param {Tine.Tinebase.data.Record} record
1092      */
1093     removeFromInvalidRelatedRecords: function(record) {
1094         if (Ext.isArray(this.view.invalidRelatedRecords)) {
1095             var index = this.view.invalidRelatedRecords.indexOf(record.id);
1096             if (index > -1) {
1097                 this.view.invalidRelatedRecords.splice(index, 1);
1098             }
1099         }
1100     },
1101     
1102     /**
1103      * removes a record from invalid records array
1104      * 
1105      * @param {Tine.Tinebase.data.Record} record
1106      */
1107     removeFromInvalidRowRecords: function(record) {
1108         if (Ext.isArray(this.view.invalidRowRecords)) {
1109             var index = this.view.invalidRowRecords.indexOf(record.id);
1110             if (index > -1) {
1111                 this.view.invalidRowRecords.splice(index, 1);
1112             }
1113         }
1114     },
1115     
1116     /**
1117      * is called when a record is added to the store
1118      * @param {Ext.data.SimpleStore} store
1119      * @param {Array} records
1120      */
1121     onAdd: function(store, records) {
1122         Ext.each(records, function(record) {
1123             Ext.each(this.editDialog.relationPickers, function(picker) {
1124                 if(picker.relationType == record.get('type') && record.get('related_id') != picker.getValue() && picker.fullModelName == record.get('related_model')) {
1125                     var split = picker.fullModelName.split('_Model_');
1126                     picker.combo.selectedRecord = new Tine[split[0]].Model[split[1]](record.get('related_record'));
1127                     picker.combo.startRecord = new Tine[split[0]].Model[split[1]](record.get('related_record'));
1128                     picker.combo.setValue(picker.combo.selectedRecord);
1129                     picker.combo.startValue = picker.combo.selectedRecord.get(this.recordClass.getMeta('idProperty'));
1130                 }
1131             }, this);
1132         }, this);
1133     },
1134     /**
1135      * is called when a record in the grid changes
1136      * @param {Ext.data.SimpleStore} store
1137      * @param {} record
1138      */
1139     onUpdate: function(store, record) {
1140         store.each(function(record) {
1141             Ext.each(this.editDialog.relationPickers, function(picker) {
1142                 if(picker.relationType == record.get('type') && picker.fullModelName == record.get('related_model')) {
1143                     picker.setValue(record.get('related_record'));
1144                 }
1145             }, this);
1146         }, this);
1147         this.updateTitle();
1148     },
1149
1150     /**
1151      * populate store and set record
1152      * @param {Record} record
1153      */
1154     loadRecord: function(dialog, record, ticketFn) {
1155         var _ = window.lodash,
1156             interceptor = ticketFn(),
1157             evalGrants = dialog.evalGrants,
1158             hasRequiredGrant = !evalGrants || _.get(record, record.constructor.getMeta('grantsPath') + '.' + this.requiredGrant);
1159
1160         this.store.removeAll();
1161
1162         if (dialog.mode == 'local' && this.editDialog.recordClass.getMeta('phpClassName') === 'Calendar_Model_Event') {
1163             // if dialog is local, relations must be fetched async
1164             Tine.Tinebase.getRelations('Calendar_Model_Event', record.get('id'), null, [], null, function (response, request) {
1165                 if(response) {
1166                     this.loadRelations(response.results, interceptor);
1167                 }
1168             }.createDelegate(this));
1169         } else {
1170             var relations = record.get('relations');
1171             this.loadRelations(relations, interceptor);
1172         }
1173
1174         this.setReadOnly(! hasRequiredGrant);
1175     },
1176
1177     loadRelations: function(relations, interceptor) {
1178         if (relations && relations.length > 0) {
1179             var relationRecords = [];
1180             
1181             Ext.each(relations, function(relation) {
1182                 if (this.ignoreRelatedModels) {
1183                     if (relation.hasOwnProperty('related_model') && this.ignoreRelatedModels.indexOf(relation.related_model) == -1) {
1184                         relationRecords.push(new Tine.Tinebase.Model.Relation(relation, relation.id));
1185                     }
1186                 } else {
1187                     relationRecords.push(new Tine.Tinebase.Model.Relation(relation, relation.id));
1188                 }
1189             }, this);
1190             this.store.add(relationRecords);
1191             
1192             // sort by creation time
1193             this.store.sort('creation_time', 'DESC');
1194             this.updateTitle();
1195         } else {
1196             this.updateTitle(0);
1197         }
1198
1199         // add other listeners after population
1200         if (this.store) {
1201             this.store.on('update', this.onUpdate, this);
1202             this.store.on('add', this.updateTitle, this);
1203             this.store.on('remove', function(store, records, index) {
1204                 Ext.each(records, function(record) {
1205                     Ext.each(this.editDialog.relationPickers, function(picker) {
1206                         if (picker.relationType == record.get('type') && record.get('related_id') == picker.getValue() && picker.fullModelName == record.get('related_model')) {
1207                             picker.clear();
1208                         }
1209                     }, this);
1210                 }, this);
1211                 this.updateTitle();
1212             }, this);
1213         }
1214         interceptor();
1215     },
1216     
1217     /**
1218      * remove handler
1219      * 
1220      * @param {} button
1221      * @param {} event
1222      */
1223     onRemove: function(button, event) {
1224         var selectedRows = this.getSelectionModel().getSelections();
1225         
1226         for (var index = 0; index < selectedRows.length; index++) {
1227             var split = selectedRows[index].get('related_model').split('_Model_');
1228             
1229             this.checkOldValidations(split[0], split[1], selectedRows[index], selectedRows[index].get('type'));
1230             
1231             this.removeFromInvalidRowRecords(selectedRows[index]);
1232             this.removeFromInvalidRelatedRecords(selectedRows[index]);
1233         }
1234         
1235         Tine.widgets.relation.GenericPickerGridPanel.superclass.onRemove.call(this, button, event);
1236     },
1237     
1238     /**
1239      * checks if there are invalid relations
1240      * @return {Boolean}
1241      */
1242     isValid: function() {
1243         if (this.view) {
1244             if (this.view.hasOwnProperty('invalidRowRecords') && (this.view.invalidRowRecords.length > 0)) {
1245                 return false;
1246             }
1247             if (this.view.hasOwnProperty('invalidRelatedRecords') && (this.view.invalidRelatedRecords.length > 0)) {
1248                 return false;
1249             }
1250         }
1251         return true;
1252     },
1253
1254     /**
1255      * get relations data as array
1256      * 
1257      * @param store if no store is given, this.store will be iterated
1258      * @return {Array}
1259      */
1260     getData: function(store) {
1261         store = store ? store : this.store;
1262         var relations = [];
1263         store.each(function(record) {
1264             record.data.related_record.relations = null;
1265             record.data.related_record.relation = null;
1266             delete record.data.related_record.relations;
1267             delete record.data.related_record.relation;
1268             relations.push(record.data);
1269         }, this);
1270
1271         return relations;
1272     }
1273 });