Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Tinebase / js / widgets / dialog / EditDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.widgets.dialog');
9
10 /**
11  * Generic 'Edit Record' dialog
12  * Base class for all 'Edit Record' dialogs
13  * 
14  * @namespace   Tine.widgets.dialog
15  * @class       Tine.widgets.dialog.EditDialog
16  * @extends     Ext.FormPanel
17  * @author      Cornelius Weiss <c.weiss@metaways.de>
18  * @constructor
19  * @param {Object} config The configuration options.
20  */
21
22 Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
23     /**
24      * @cfg {Tine.Tinebase.Application} app
25      * instance of the app object (required)
26      */
27     app: null,
28     /**
29      * @cfg {String} mode
30      * Set to 'local' if the EditDialog only operates on this.record (defaults to 'remote' which loads and saves using the recordProxy)
31      */
32     mode : 'remote',
33     /**
34      * @cfg {Array} tbarItems
35      * additional toolbar items (defaults to false)
36      */
37     tbarItems: false,
38     /**
39      * internal/untranslated app name (required)
40      * 
41      * @cfg {String} appName
42      */
43     appName: null,
44     /**
45      * the modelName (filled by application starter)
46      * 
47      * @type {String} modelName
48      */
49     modelName: null,
50     
51     /**
52      * record definition class  (required)
53      * 
54      * @cfg {Ext.data.Record} recordClass
55      */
56     recordClass: null,
57     /**
58      * @cfg {Ext.data.DataProxy} recordProxy
59      */
60     recordProxy: null,
61     /**
62      * @cfg {Bool} showContainerSelector
63      * show container selector in bottom area
64      */
65     showContainerSelector: null,
66     /**
67      * @cfg {Bool} evalGrants
68      * should grants of a grant-aware records be evaluated (defaults to true)
69      */
70     evalGrants: true,
71     /**
72      * @cfg {Ext.data.Record} record
73      * record in edit process.
74      */
75     record: null,
76     
77     /**
78      * holds the modelConfig for the handled record (json-encoded object)
79      * will be decoded in initComponent
80      * 
81      * @type 
82      */
83     modelConfig: null,
84     
85     /**
86      * @cfg {String} saveAndCloseButtonText
87      * text of save and close button
88      */
89     saveAndCloseButtonText: '',
90     /**
91      * @cfg {String} cancelButtonText
92      * text of cancel button
93      */
94     cancelButtonText: '',
95     
96     /**
97      * @cfg {Boolean} copyRecord
98      * copy record
99      */
100     copyRecord: false,
101     
102     /**
103      * @cfg {Boolean} doDuplicateCheck
104      * do duplicate check when saving record (mode remote only)
105      */
106     doDuplicateCheck: true,
107     
108     /**
109      * required grant for apply/save
110      * @type String
111      */
112     editGrant: 'editGrant',
113
114     /**
115      * when a record has the relations-property the relations-panel can be disabled here
116      * @cfg {Boolean} hideRelationsPanel
117      */
118     hideRelationsPanel: false,
119     
120     /**
121      * when a record has the attachments-property the attachments-panel can be disabled here
122      * @cfg {Boolean} hideAttachmentsPanel
123      */
124     hideAttachmentsPanel: false,
125     
126     /**
127      * Registry for other relationgridpanels than the generic one,
128      * handling special types of relations the generic one will not.
129      * Panels registered here must have a store with the relation records.
130      * 
131      * @type {Array}
132      */
133     relationPanelRegistry: null,
134     
135     /**
136      * ignore relations to given php-class names in the relation grid
137      * @type {Array}
138      */
139     ignoreRelatedModels: null,
140     
141     /**
142      * dialog is currently saving data
143      * @type Boolean
144      */
145     saving: false,
146     
147     /**
148      * Disable adding cf tab even if model has support for customfields
149      * @type Boolean
150      */
151     disableCfs: false,
152     
153     /**
154      * @property window {Ext.Window|Ext.ux.PopupWindow|Ext.Air.Window}
155      */
156     /**
157      * @property {Number} loadRequest 
158      * transaction id of loadData request
159      */
160     /**
161      * @property loadMask {Ext.LoadMask}
162      */
163     
164     /**
165      * @property containerSelectCombo {Tine.widgets.container.selectionComboBox}
166      */
167     containerSelectCombo: null,
168     
169     /**
170      * If set, these fields are readOnly (when called dependent to related record)
171      * 
172      * @type {Ext.util.MixedCollection}
173      */
174     fixedFields: null,
175
176     /**
177      * Plain Object with additional configuration (JSON-encoded)
178      * 
179      * @type {Object}
180      */
181     additionalConfig: null,
182     
183     // private
184     bodyStyle:'padding:5px',
185     layout: 'fit',
186     border: false,
187     cls: 'tw-editdialog',
188     anchor:'100% 100%',
189     deferredRender: false,
190     buttonAlign: null,
191     bufferResize: 500,
192
193     /**
194      * relations panel
195      * 
196      * @type Tine.widgets.relation.GenericPickerGridPanel
197      */
198     relationsPanel: null,
199     
200     // Array of Relation Pickers
201     relationPickers: null,
202     
203     /**
204      * attachments panel
205      * 
206      * @type Tine.widgets.dialog.AttachmentsGridPanel
207      */
208     attachmentsPanel: null,
209     
210     /**
211      * holds the loadMask
212      * set this to false, if no loadMask should be shown
213      * 
214      * @type {Ext.LoadMask}
215      */
216     loadMask: null,
217     
218     //private
219     initComponent: function() {
220         this.relationPanelRegistry = this.relationPanelRegistry ? this.relationPanelRegistry : [];
221         this.addEvents(
222             /**
223              * @event cancel
224              * Fired when user pressed cancel button
225              */
226             'cancel',
227             /**
228              * @event saveAndClose
229              * Fired when user pressed OK button
230              */
231             'saveAndClose',
232             /**
233              * @event update
234              * @desc  Fired when the record got updated
235              * @param {Json String} data data of the entry
236              * @pram  {String} this.mode
237              */
238             'update',
239             /**
240              * @event apply
241              * Fired when user pressed apply button
242              */
243             'apply',
244             /**
245              * @event load
246              * @param {Tine.widgets.dialog.EditDialog} this
247              * @param {Tine.data.Record} record which got loaded
248              * @param {Function} ticket function for async defer
249              * Fired when record is loaded
250              */
251             'load',
252             /**
253              * @event save
254              * @param {Tine.widgets.dialog.EditDialog} this
255              * @param {Tine.data.Record} record which got loaded
256              * @param {Function} ticket function for async defer
257              * Fired when remote record is saving
258              */
259             'save',
260             /**
261              * @event updateDependent
262              * Fired when a subpanel updates the record locally
263              */
264             'updateDependent'
265         );
266         
267         if (Ext.isString(this.modelConfig)) {
268             this.modelConfig = Ext.decode(this.modelConfig);
269         }
270         
271         if (Ext.isString(this.additionalConfig)) {
272             Ext.apply(this, Ext.decode(this.additionalConfig));
273         }
274         
275         if (Ext.isString(this.fixedFields)) {
276             var decoded = Ext.decode(this.fixedFields);
277             this.fixedFields = new Ext.util.MixedCollection();
278             this.fixedFields.addAll(decoded);
279         }
280         
281         if (! this.recordClass && this.modelName) {
282             this.recordClass = Tine[this.appName].Model[this.modelName];
283         }
284         
285         if (this.recordClass) {
286             this.appName    = this.appName    ? this.appName    : this.recordClass.getMeta('appName');
287             this.modelName  = this.modelName  ? this.modelName  : this.recordClass.getMeta('modelName');
288         }
289         
290         if (! this.app) {
291             this.app = Tine.Tinebase.appMgr.get(this.appName);
292         }
293         
294         if (! this.windowNamePrefix) {
295             this.windowNamePrefix = this.modelName + 'EditWindow_';
296         }
297         
298         Tine.log.debug('initComponent: appName: ', this.appName);
299         Tine.log.debug('initComponent: modelName: ', this.modelName);
300         Tine.log.debug('initComponent: app: ', this.app);
301         
302         // init some translations
303         if (this.app.i18n && this.recordClass !== null) {
304             this.i18nRecordName = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), 1);
305             this.i18nRecordsName = this.app.i18n._hidden(this.recordClass.getMeta('recordsName'));
306         }
307     
308         if (! this.recordProxy && this.recordClass) {
309             Tine.log.debug('no record proxy given, creating a new one...');
310             this.recordProxy = new Tine.Tinebase.data.RecordProxy({
311                 recordClass: this.recordClass
312             });
313         }
314         // init plugins
315         this.plugins = Ext.isString(this.plugins) ? Ext.decode(this.plugins) : Ext.isArray(this.plugins) ? this.plugins.concat(Ext.decode(this.initialConfig.plugins)) : [];
316         
317         this.plugins.push(this.tokenModePlugin = new Tine.widgets.dialog.TokenModeEditDialogPlugin({}));
318         // added possibility to disable using customfield plugin
319         if (this.disableCfs !== true) {
320             this.plugins.push(new Tine.widgets.customfields.EditDialogPlugin({}));
321         }
322         
323         // init actions
324         this.initActions();
325         // init buttons and tbar
326         this.initButtons();
327         // init container selector
328         this.initContainerSelector();
329         // init record 
330         this.initRecord();
331         // get items for this dialog
332         this.items = this.getFormItems();
333         
334         // init relations panel if relations are defined
335         this.initRelationsPanel();
336         // init attachments panel
337         this.initAttachmentsPanel();
338
339         Tine.widgets.dialog.EditDialog.superclass.initComponent.call(this);
340         
341         // set fields readOnly if set
342         this.fixFields();
343         
344         // firefox fix: blur each item before tab changes, so no field  will be focused afterwards
345         if (Ext.isGecko) {
346             this.items.items[0].addListener('beforetabchange', function(tabpanel, newtab, oldtab) {
347                 if (! oldtab) {
348                     return;
349                 }
350                 var form = this.getForm();
351                 
352                 if (form && form.hasOwnProperty('items'))
353                     form.items.each(function(item, index) {
354                         item.blur();
355                     });
356             }, this);
357         }
358     },
359
360     /**
361      * fix fields (used for preselecting form fields when called in dependency to another record)
362      * @return {Boolean}
363      */
364     fixFields: function() {
365         if (this.fixedFields && this.fixedFields.getCount() > 0) {
366             if (! this.rendered) {
367                 this.fixFields.defer(100, this);
368                 return false;
369             }
370             
371             this.fixedFields.each(function(value, index) {
372                 var key = this.fixedFields.keys[index]; 
373                 
374                 var field = this.getForm().findField(key);
375                 
376                 if (field) {
377                     if (Ext.isFunction(this.recordClass.getField(key).type)) {
378                         var foreignRecordClass = this.recordClass.getField(key).type;
379                         var record = new foreignRecordClass(value);
380                         field.selectedRecord = record;
381                         field.setValue(value);
382                         field.fireEvent('select');
383                     } else {
384                         field.setValue(value);
385                     }
386                     field.disable();
387                 }
388             }, this);
389         }
390     },
391
392     /**
393      * Get available model for given application
394      *
395      *  @param {Mixed} application
396      *  @param {Boolean} customFieldModel
397      */
398     getApplicationModels: function (application, customFieldModel) {
399         var models      = [],
400             useModel,
401             appName     = Ext.isString(application) ? application : application.get('name'),
402             app         = Tine.Tinebase.appMgr.get(appName),
403             trans       = app && app.i18n ? app.i18n : Tine.Tinebase.translation,
404             appModels   = Tine[appName].Model;
405
406         if (appModels) {
407             for (var model in appModels) {
408                 if (appModels.hasOwnProperty(model) && typeof appModels[model].getMeta === 'function') {
409                     if (customFieldModel && appModels[model].getField('customfields')) {
410                         useModel = appModels[model].getMeta('appName') + '_Model_' + appModels[model].getMeta('modelName');
411
412                         Tine.log.info('Found model with customfields property: ' + useModel);
413                         models.push([useModel, trans.n_(appModels[model].getMeta('recordName'), appModels[model].getMeta('recordsName'), 1)]);
414                     } else if (! customFieldModel) {
415                         useModel = 'Tine.' + appModels[model].getMeta('appName') + '.Model.' + appModels[model].getMeta('modelName');
416
417                         Tine.log.info('Found model: ' + useModel);
418                         models.push([useModel, trans.n_(appModels[model].getMeta('recordName'), appModels[model].getMeta('recordsName'), 1)]);
419                     }
420                 }
421             }
422         }
423         return models;
424     },
425
426     /**
427      * init actions
428      */
429     initActions: function() {
430         this.action_saveAndClose = new Ext.Action({
431             requiredGrant: this.editGrant,
432             text: (this.saveAndCloseButtonText != '') ? this.app.i18n._(this.saveAndCloseButtonText) : _('Ok'),
433             minWidth: 70,
434             ref: '../btnSaveAndClose',
435             scope: this,
436             // TODO: remove the defer when all subpanels use the deferByTicket mechanism
437             handler: function() { this.onSaveAndClose.defer(500, this); },
438             iconCls: 'action_saveAndClose'
439         });
440     
441         this.action_applyChanges = new Ext.Action({
442             requiredGrant: this.editGrant,
443             text: _('Apply'),
444             minWidth: 70,
445             ref: '../btnApplyChanges',
446             scope: this,
447             handler: this.onApplyChanges,
448             iconCls: 'action_applyChanges'
449         });
450         
451         this.action_cancel = new Ext.Action({
452             text: (this.cancelButtonText != '') ? this.app.i18n._(this.cancelButtonText) : _('Cancel'),
453             minWidth: 70,
454             scope: this,
455             handler: this.onCancel,
456             iconCls: 'action_cancel'
457         });
458         
459         this.action_delete = new Ext.Action({
460             requiredGrant: 'deleteGrant',
461             text: _('delete'),
462             minWidth: 70,
463             scope: this,
464             handler: this.onDelete,
465             iconCls: 'action_delete',
466             disabled: true
467         });
468     },
469     
470     /**
471      * init buttons
472      * 
473      * use button order from preference
474      */
475     initButtons: function () {
476         this.fbar = [
477             '->'
478         ];
479                 
480         if (Tine.Tinebase.registry && Tine.Tinebase.registry.get('preferences') && Tine.Tinebase.registry.get('preferences').get('dialogButtonsOrderStyle') === 'Windows') {
481             this.fbar.push(this.action_saveAndClose, this.action_cancel);
482         } else {
483             this.fbar.push(this.action_cancel, this.action_saveAndClose);
484         }
485        
486         if (this.tbarItems) {
487             this.tbar = new Ext.Toolbar({
488                 items: this.tbarItems
489             });
490         }
491     },
492     
493     /**
494      * init container selector
495      */
496     initContainerSelector: function() {
497         if (this.showContainerSelector) {
498             this.containerSelectCombo = new Tine.widgets.container.selectionComboBox({
499                 id: this.app.appName + 'EditDialogContainerSelector-' + Ext.id(),
500                 fieldLabel: _('Saved in'),
501                 width: 300,
502                 listWidth: 300,
503                 name: this.recordClass.getMeta('containerProperty'),
504                 recordClass: this.recordClass,
505                 containerName: this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1),
506                 containersName: this.app.i18n._hidden(this.recordClass.getMeta('containersName')),
507                 appName: this.app.appName,
508                 requiredGrant: this.evalGrants ? 'addGrant' : false,
509                 disabled: this.isContainerSelectorDisabled(),
510                 listeners: {
511                     scope: this,
512                     select: function() {    
513                         // enable or disable save button dependent to containers account grants
514                         var grants = this.containerSelectCombo.selectedContainer ? this.containerSelectCombo.selectedContainer.account_grants : {};
515                         // on edit check editGrant, on add check addGrant
516                         if (this.record.data.id) {  // edit if record has already an id
517                             var disable = grants.hasOwnProperty('editGrant') ? ! grants.editGrant : false;
518                         } else {
519                             var disable = grants.hasOwnProperty('addGrant') ? ! grants.addGrant : false;
520                         }
521                         this.action_saveAndClose.setDisabled(disable);
522                     }
523                 }
524             });
525             this.on('render', function() { this.getForm().add(this.containerSelectCombo); }, this);
526             
527             this.fbar = [
528                 _('Saved in'),
529                 this.containerSelectCombo
530             ].concat(this.fbar);
531         }
532         
533     },
534     
535     /**
536      * checks if the container selector should be disabled (dependent on account grants of the container itself)
537      * @return {}
538      */
539     isContainerSelectorDisabled: function() {
540         if (this.record) {
541             var cp = this.recordClass.getMeta('containerProperty'),
542                 container = this.record.data[cp],
543                 grants = (container && container.hasOwnProperty('account_grants')) ? container.account_grants : null,
544                 cond = false;
545                 
546             // check grants if record already exists and grants should be evaluated
547             if(this.evalGrants && this.record.data.id && grants) {
548                 cond = ! (grants.hasOwnProperty('editGrant') && grants.editGrant);
549             }
550             
551             return cond;
552         } else {
553             return false;
554         }
555     },
556     
557     /**
558      * init record to edit
559      */
560     initRecord: function() {
561         Tine.log.debug('init record with mode: ' + this.mode);
562         if (! this.record) {
563             Tine.log.debug('creating new default data record');
564             this.record = new this.recordClass(this.recordClass.getDefaultData(), 0);
565         }
566         
567         if (this.mode !== 'local') {
568             if (this.record && this.record.id) {
569                 this.loadRemoteRecord();
570             } else {
571                 this.onRecordLoad();
572             }
573         } else {
574             // note: in local mode we expect a valid record
575             if (! Ext.isFunction(this.record.beginEdit)) {
576                 this.record = this.recordProxy.recordReader({responseText: this.record});
577             }
578             this.onRecordLoad();
579         }
580     },
581     
582     /**
583      * load record via record proxy
584      */
585     loadRemoteRecord: function() {
586         Tine.log.info('initiating record load via proxy');
587         this.loadRequest = this.recordProxy.loadRecord(this.record, {
588             scope: this,
589             success: function(record) {
590                 this.record = record;
591                 this.onRecordLoad();
592             }
593         });
594     },
595
596     /**
597      * copy this.record record
598      */
599     doCopyRecord: function() {
600         this.record = this.doCopyRecordToReturn(this.record);
601     },
602
603     /**
604      * Copy record and returns "new record with same settings"
605      *
606      * @param record
607      */
608     doCopyRecordToReturn: function(record) {
609         var omitFields = this.recordClass.getMeta('copyOmitFields') || [];
610         // always omit id + notes + attachments
611         omitFields = omitFields.concat(['id', 'notes', 'attachments', 'relations']);
612
613         var fieldsToCopy = this.recordClass.getFieldNames().diff(omitFields),
614             recordData = Ext.copyTo({}, record.data, fieldsToCopy);
615
616         var resetProperties = {
617             alarms:    ['id', 'record_id', 'sent_time', 'sent_message'],
618             relations: ['id', 'own_id', 'created_by', 'creation_time', 'last_modified_by', 'last_modified_time']
619         };
620
621         var setProperties = {alarms: {sent_status: 'pending'}};
622
623         Ext.iterate(resetProperties, function(property, properties) {
624             if (recordData.hasOwnProperty(property)) {
625                 var r = recordData[property];
626                 for (var index = 0; index < r.length; index++) {
627                     Ext.each(properties,
628                         function(prop) {
629                             r[index][prop] = null;
630                         }
631                     );
632                 }
633             }
634         });
635
636         Ext.iterate(setProperties, function(property, properties) {
637             if (recordData.hasOwnProperty(property)) {
638                 var r = recordData[property];
639                 for (var index = 0; index < r.length; index++) {
640                     Ext.iterate(properties,
641                         function(prop, value) {
642                             r[index][prop] = value;
643                         }
644                     );
645                 }
646             }
647         });
648
649         return new this.recordClass(recordData, 0);
650     },
651
652     
653     /**
654      * executed after record got updated from proxy
655      */
656     onRecordLoad: function() {
657         // interrupt process flow until dialog is rendered
658         if (! this.rendered) {
659             this.onRecordLoad.defer(250, this);
660             return;
661         }
662         Tine.log.debug('Tine.widgets.dialog.EditDialog::onRecordLoad() - Loading of the following record completed:');
663         Tine.log.debug(this.record);
664         
665         if (this.copyRecord) {
666             this.doCopyRecord();
667             this.window.setTitle(String.format(_('Copy {0}'), this.i18nRecordName));
668         } else {
669             if (! this.record.id) {
670                 this.window.setTitle(String.format(_('Add New {0}'), this.i18nRecordName));
671             } else {
672                 this.window.setTitle(String.format(_('Edit {0} "{1}"'), this.i18nRecordName, this.record.getTitle()));
673             }
674         }
675         
676         var ticketFn = this.onAfterRecordLoad.deferByTickets(this),
677             wrapTicket = ticketFn();
678         
679         this.fireEvent('load', this, this.record, ticketFn);
680         wrapTicket();
681     },
682     
683     // finally load the record into the form
684     onAfterRecordLoad: function() {
685         var form = this.getForm();
686         
687         if (form) {
688             form.loadRecord(this.record);
689             form.clearInvalid();
690         }
691         
692         if (this.record && this.record.hasOwnProperty('data') && Ext.isObject(this.record.data[this.recordClass.getMeta('containerProperty')])) {
693             this.updateToolbars(this.record, this.recordClass.getMeta('containerProperty'));
694         }
695         
696         // add current timestamp as id, if this is a dependent record 
697         if (this.modelConfig && this.modelConfig.isDependent == true && this.record.id == 0) {
698             this.record.set('id', (new Date()).getTime());
699         }
700         
701         if (this.loadMask) {
702             this.loadMask.hide();
703         }
704     },
705     
706     /**
707      * executed when record gets updated from form
708      */
709     onRecordUpdate: function() {
710         var form = this.getForm();
711
712         // merge changes from form into record
713         form.updateRecord(this.record);
714     },
715     
716     /**
717      * @private
718      */
719     onRender : function(ct, position){
720         Tine.widgets.dialog.EditDialog.superclass.onRender.call(this, ct, position);
721         
722         // generalized keybord map for edit dlgs
723         new Ext.KeyMap(this.el, [
724             {
725                 key: [10,13], // ctrl + return
726                 ctrl: true,
727                 scope: this,
728                 fn: function() {
729                     if (this.getForm().hasOwnProperty('items')) {
730                         // force set last selected field
731                         this.getForm().items.each(function(item) {
732                             if (item.hasFocus) {
733                                 item.setValue(item.getRawValue());
734                             }
735                         }, this);
736                     }
737                     this.action_saveAndClose.execute();
738                 }
739             }
740         ]);
741         
742         if (this.loadMask !== false && this.i18nRecordName) {
743             this.loadMask = new Ext.LoadMask(ct, {msg: String.format(_('Transferring {0}...'), this.i18nRecordName)});
744             this.loadMask.show();
745         }
746     },
747     
748     /**
749      * update (action updateer) top and bottom toolbars
750      */
751     updateToolbars: function(record, containerField) {
752         if (! this.evalGrants) {
753             return;
754         }
755         
756         var actions = [
757             this.action_saveAndClose,
758             this.action_applyChanges,
759             this.action_delete,
760             this.action_cancel
761         ];
762         Tine.widgets.actionUpdater(record, actions, containerField);
763         Tine.widgets.actionUpdater(record, this.tbarItems, containerField);
764     },
765     
766     /**
767      * get top toolbar
768      */
769     getToolbar: function() {
770         return this.getTopToolbar();
771     },
772     
773     /**
774      * is form valid?
775      * 
776      * @return {Boolean}
777      */
778     isValid: function() {
779         return this.getForm().isValid();
780     },
781     
782     /**
783      * vaidates on multiple edit
784      * 
785      * @return {Boolean}
786      */
787     isMultipleValid: function() {
788         return true;
789     },
790     
791     /**
792      * @private
793      */
794     onCancel: function(){
795         this.fireEvent('cancel');
796         this.purgeListeners();
797         this.window.close();
798     },
799     
800     /**
801      * @private
802      */
803     onSaveAndClose: function() {
804         this.fireEvent('saveAndClose');
805         this.onApplyChanges(true);
806     },
807     
808     /**
809      * generic apply changes handler
810      * @param {Boolean} closeWindow
811      */
812     onApplyChanges: function(closeWindow) {
813         if (this.saving) {
814             return;
815         }
816         this.saving = true;
817
818         this.loadMask.show();
819         
820         var ticketFn = this.doApplyChanges.deferByTickets(this, [closeWindow]),
821             wrapTicket = ticketFn();
822
823         this.fireEvent('save', this, this.record, ticketFn);
824         wrapTicket();
825     },
826     
827     /**
828      * is called from onApplyChanges
829      * @param {Boolean} closeWindow
830      */
831     doApplyChanges: function(closeWindow) {
832         // we need to sync record before validating to let (sub) panels have 
833         // current data of other panels
834         this.onRecordUpdate();
835         
836         // quit copy mode
837         this.copyRecord = false;
838         
839         if (this.isValid()) {
840             if (this.mode !== 'local') {
841                 this.recordProxy.saveRecord(this.record, {
842                     scope: this,
843                     success: function(record) {
844                         // override record with returned data
845                         this.record = record;
846                         if (! Ext.isFunction(this.window.cascade)) {
847                             // update form with this new data
848                             // NOTE: We update the form also when window should be closed,
849                             //       cause sometimes security restrictions might prevent
850                             //       closing of native windows
851                             this.onRecordLoad();
852                         }
853                         var ticketFn = this.onAfterApplyChanges.deferByTickets(this, [closeWindow]),
854                             wrapTicket = ticketFn();
855                             
856                         this.fireEvent('update', Ext.util.JSON.encode(this.record.data), this.mode, this, ticketFn);
857                         wrapTicket();
858                     },
859                     failure: this.onRequestFailed,
860                     timeout: 300000 // 5 minutes
861                 }, {
862                     duplicateCheck: this.doDuplicateCheck
863                 });
864             } else {
865                 this.onRecordLoad();
866                 var ticketFn = this.onAfterApplyChanges.deferByTickets(this, [closeWindow]),
867                     wrapTicket = ticketFn();
868                     
869                 this.fireEvent('update', Ext.util.JSON.encode(this.record.data), this.mode, this, ticketFn);
870                 wrapTicket();
871             }
872         } else {
873             this.saving = false;
874             this.loadMask.hide();
875             Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
876         }
877     },
878     
879     onAfterApplyChanges: function(closeWindow) {
880         this.window.rename(this.windowNamePrefix + this.record.id);
881         this.loadMask.hide();
882         this.saving = false;
883         
884         if (closeWindow) {
885             this.window.fireEvent('saveAndClose');
886             this.purgeListeners();
887             this.window.close();
888         }
889     },
890     
891     /**
892      * get validation error message
893      * 
894      * @return {String}
895      */
896     getValidationErrorMessage: function() {
897         return _('Please fix the errors noted.');
898     },
899     
900     /**
901      * generic delete handler
902      */
903     onDelete: function(btn, e) {
904         Ext.MessageBox.confirm(_('Confirm'), String.format(_('Do you really want to delete this {0}?'), this.i18nRecordName), function(_button) {
905             if(btn == 'yes') {
906                 var deleteMask = new Ext.LoadMask(this.getEl(), {msg: String.format(_('Deleting {0}'), this.i18nRecordName)});
907                 deleteMask.show();
908                 
909                 this.recordProxy.deleteRecords(this.record, {
910                     scope: this,
911                     success: function() {
912                         this.purgeListeners();
913                         this.window.close();
914                     },
915                     failure: function () {
916                         Ext.MessageBox.alert(_('Failed'), String.format(_('Could not delete {0}.'), this.i18nRecordName));
917                         Ext.MessageBox.hide();
918                     }
919                 });
920             }
921         });
922     },
923     
924     /**
925      * duplicate(s) found exception handler
926      * 
927      * @param {Object} exception
928      */
929     onDuplicateException: function(exception) {
930         var resolveGridPanel = new Tine.widgets.dialog.DuplicateResolveGridPanel({
931             app: this.app,
932             store: new Tine.widgets.dialog.DuplicateResolveStore({
933                 app: this.app,
934                 recordClass: this.recordClass,
935                 recordProxy: this.recordProxy,
936                 data: {
937                     clientRecord: exception.clientRecord,
938                     duplicates: exception.duplicates
939                 }
940             }),
941             fbar: [
942                 '->',
943                 this.action_cancel,
944                 this.action_saveAndClose
945             ]
946         });
947         
948         // intercept save handler
949         resolveGridPanel.btnSaveAndClose.setHandler(function(btn, e) {
950             var resolveStrategy = resolveGridPanel.store.resolveStrategy;
951             
952             // action discard -> close window
953             if (resolveStrategy == 'discard') {
954                 return this.onCancel();
955             }
956             
957             this.record = resolveGridPanel.store.getResolvedRecord();
958             
959             // quit copy mode before populating form with resolved data
960             this.copyRecord = false;
961             this.onRecordLoad();
962             
963             mainCardPanel.layout.setActiveItem(this.id);
964             resolveGridPanel.doLayout();
965             
966             this.doDuplicateCheck = false;
967             this.onSaveAndClose();
968         }, this);
969         
970         // place in viewport
971         this.window.setTitle(String.format(_('Resolve Duplicate {0} Suspicion'), this.i18nRecordName));
972         var mainCardPanel = this.findParentBy(function(p) {return p.isWindowMainCardPanel });
973         mainCardPanel.add(resolveGridPanel);
974         mainCardPanel.layout.setActiveItem(resolveGridPanel.id);
975         resolveGridPanel.doLayout();
976     },
977     
978     /**
979      * generic request exception handler
980      * 
981      * @param {Object} exception
982      */
983     onRequestFailed: function(exception) {
984         this.saving = false;
985         
986         if (exception.code == 629) {
987             this.onDuplicateException.apply(this, arguments);
988         } else {
989             Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
990         }
991         this.loadMask.hide();
992     },
993     
994     /**
995      * creates the relations panel, if relations are defined
996      */
997     initRelationsPanel: function() {
998         if (! this.hideRelationsPanel && this.recordClass && this.recordClass.hasField('relations')) {
999             // init relations panel before onRecordLoad
1000             if (! this.relationsPanel) {
1001                 this.relationsPanel = new Tine.widgets.relation.GenericPickerGridPanel({ anchor: '100% 100%', editDialog: this });
1002             }
1003             // interrupt process flow until dialog is rendered
1004             if (! this.rendered) {
1005                 this.initRelationsPanel.defer(250, this);
1006                 return;
1007             }
1008             // add relations panel if this is rendered
1009             if (this.items.items[0]) {
1010                 this.items.items[0].add(this.relationsPanel);
1011             }
1012             
1013             Tine.log.debug('Tine.widgets.dialog.EditDialog::initRelationsPanel() - Initialized relations panel and added to dialog tab items.');
1014         }
1015     },
1016     
1017     /**
1018      * creates attachments panel
1019      */
1020     initAttachmentsPanel: function() {
1021         if (! this.attachmentsPanel && ! this.hideAttachmentsPanel && this.recordClass && this.recordClass.hasField('attachments') && Tine.Tinebase.registry.get('filesystemAvailable')) {
1022             this.attachmentsPanel = new Tine.widgets.dialog.AttachmentsGridPanel({ anchor: '100% 100%', editDialog: this }); 
1023             this.items.items.push(this.attachmentsPanel);
1024         }
1025     }
1026 });