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