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)
8 Ext.ns('Tine.widgets.dialog');
11 * Generic 'Edit Record' dialog
12 * Base class for all 'Edit Record' dialogs
14 * @namespace Tine.widgets.dialog
15 * @class Tine.widgets.dialog.EditDialog
16 * @extends Ext.FormPanel
17 * @author Cornelius Weiss <c.weiss@metaways.de>
19 * @param {Object} config The configuration options.
22 Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
24 * @cfg {Tine.Tinebase.Application} app
25 * instance of the app object (required)
30 * Set to 'local' if the EditDialog only operates on this.record (defaults to 'remote' which loads and saves using the recordProxy)
34 * @cfg {Array} tbarItems
35 * additional toolbar items (defaults to false)
39 * internal/untranslated app name (required)
41 * @cfg {String} appName
45 * the modelName (filled by application starter)
47 * @type {String} modelName
52 * record definition class (required)
54 * @cfg {Ext.data.Record} recordClass
58 * @cfg {Ext.data.DataProxy} recordProxy
62 * @cfg {Bool} showContainerSelector
63 * show container selector in bottom area
65 showContainerSelector: null,
67 * @cfg {Bool} evalGrants
68 * should grants of a grant-aware records be evaluated (defaults to true)
72 * @cfg {Ext.data.Record} record
73 * record in edit process.
78 * holds the modelConfig for the handled record (json-encoded object)
79 * will be decoded in initComponent
86 * @cfg {String} saveAndCloseButtonText
87 * text of save and close button
89 saveAndCloseButtonText: '',
91 * @cfg {String} cancelButtonText
92 * text of cancel button
97 * @cfg {Boolean} copyRecord
103 * @cfg {Boolean} doDuplicateCheck
104 * do duplicate check when saving record (mode remote only)
106 doDuplicateCheck: true,
109 * required grant for apply/save
112 editGrant: 'editGrant',
115 * when a record has the relations-property the relations-panel can be disabled here
116 * @cfg {Boolean} hideRelationsPanel
118 hideRelationsPanel: false,
121 * when a record has the attachments-property the attachments-panel can be disabled here
122 * @cfg {Boolean} hideAttachmentsPanel
124 hideAttachmentsPanel: false,
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.
133 relationPanelRegistry: null,
136 * ignore relations to given php-class names in the relation grid
139 ignoreRelatedModels: null,
142 * dialog is currently saving data
148 * Disable adding cf tab even if model has support for customfields
154 * @property window {Ext.Window|Ext.ux.PopupWindow|Ext.Air.Window}
157 * @property {Number} loadRequest
158 * transaction id of loadData request
161 * @property loadMask {Ext.LoadMask}
165 * @property containerSelectCombo {Tine.widgets.container.selectionComboBox}
167 containerSelectCombo: null,
170 * If set, these fields are readOnly (when called dependent to related record)
172 * @type {Ext.util.MixedCollection}
177 * Plain Object with additional configuration (JSON-encoded)
181 additionalConfig: null,
184 bodyStyle:'padding:5px',
187 cls: 'tw-editdialog',
189 deferredRender: false,
196 * @type Tine.widgets.relation.GenericPickerGridPanel
198 relationsPanel: null,
200 // Array of Relation Pickers
201 relationPickers: null,
206 * @type Tine.widgets.dialog.AttachmentsGridPanel
208 attachmentsPanel: null,
212 * set this to false, if no loadMask should be shown
214 * @type {Ext.LoadMask}
219 initComponent: function() {
220 this.relationPanelRegistry = this.relationPanelRegistry ? this.relationPanelRegistry : [];
224 * Fired when user pressed cancel button
228 * @event saveAndClose
229 * Fired when user pressed OK button
234 * @desc Fired when the record got updated
235 * @param {Json String} data data of the entry
236 * @pram {String} this.mode
241 * Fired when user pressed apply button
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
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
261 * @event updateDependent
262 * Fired when a subpanel updates the record locally
267 if (Ext.isString(this.modelConfig)) {
268 this.modelConfig = Ext.decode(this.modelConfig);
271 if (Ext.isString(this.additionalConfig)) {
272 Ext.apply(this, Ext.decode(this.additionalConfig));
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);
281 if (! this.recordClass && this.modelName) {
282 this.recordClass = Tine[this.appName].Model[this.modelName];
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');
291 this.app = Tine.Tinebase.appMgr.get(this.appName);
294 if (! this.windowNamePrefix) {
295 this.windowNamePrefix = this.modelName + 'EditWindow_';
298 Tine.log.debug('initComponent: appName: ', this.appName);
299 Tine.log.debug('initComponent: modelName: ', this.modelName);
300 Tine.log.debug('initComponent: app: ', this.app);
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'));
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
315 this.plugins = Ext.isString(this.plugins) ? Ext.decode(this.plugins) : Ext.isArray(this.plugins) ? this.plugins.concat(Ext.decode(this.initialConfig.plugins)) : [];
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({}));
325 // init buttons and tbar
327 // init container selector
328 this.initContainerSelector();
331 // get items for this dialog
332 this.items = this.getFormItems();
334 // init relations panel if relations are defined
335 this.initRelationsPanel();
336 // init attachments panel
337 this.initAttachmentsPanel();
339 Tine.widgets.dialog.EditDialog.superclass.initComponent.call(this);
341 // set fields readOnly if set
344 // firefox fix: blur each item before tab changes, so no field will be focused afterwards
346 this.items.items[0].addListener('beforetabchange', function(tabpanel, newtab, oldtab) {
350 var form = this.getForm();
352 if (form && form.hasOwnProperty('items'))
353 form.items.each(function(item, index) {
361 * fix fields (used for preselecting form fields when called in dependency to another record)
364 fixFields: function() {
365 if (this.fixedFields && this.fixedFields.getCount() > 0) {
366 if (! this.rendered) {
367 this.fixFields.defer(100, this);
371 this.fixedFields.each(function(value, index) {
372 var key = this.fixedFields.keys[index];
374 var field = this.getForm().findField(key);
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');
384 field.setValue(value);
393 * Get available model for given application
395 * @param {Mixed} application
396 * @param {Boolean} customFieldModel
398 getApplicationModels: function (application, customFieldModel) {
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;
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');
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');
417 Tine.log.info('Found model: ' + useModel);
418 models.push([useModel, trans.n_(appModels[model].getMeta('recordName'), appModels[model].getMeta('recordsName'), 1)]);
429 initActions: function() {
430 this.action_saveAndClose = new Ext.Action({
431 requiredGrant: this.editGrant,
432 text: (this.saveAndCloseButtonText != '') ? this.app.i18n._(this.saveAndCloseButtonText) : _('Ok'),
434 ref: '../btnSaveAndClose',
436 // TODO: remove the defer when all subpanels use the deferByTicket mechanism
437 handler: function() { this.onSaveAndClose.defer(500, this); },
438 iconCls: 'action_saveAndClose'
441 this.action_applyChanges = new Ext.Action({
442 requiredGrant: this.editGrant,
445 ref: '../btnApplyChanges',
447 handler: this.onApplyChanges,
448 iconCls: 'action_applyChanges'
451 this.action_cancel = new Ext.Action({
452 text: (this.cancelButtonText != '') ? this.app.i18n._(this.cancelButtonText) : _('Cancel'),
455 handler: this.onCancel,
456 iconCls: 'action_cancel'
459 this.action_delete = new Ext.Action({
460 requiredGrant: 'deleteGrant',
464 handler: this.onDelete,
465 iconCls: 'action_delete',
473 * use button order from preference
475 initButtons: function () {
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);
483 this.fbar.push(this.action_cancel, this.action_saveAndClose);
486 if (this.tbarItems) {
487 this.tbar = new Ext.Toolbar({
488 items: this.tbarItems
494 * init container selector
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'),
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(),
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;
519 var disable = grants.hasOwnProperty('addGrant') ? ! grants.addGrant : false;
521 this.action_saveAndClose.setDisabled(disable);
525 this.on('render', function() { this.getForm().add(this.containerSelectCombo); }, this);
529 this.containerSelectCombo
536 * checks if the container selector should be disabled (dependent on account grants of the container itself)
539 isContainerSelectorDisabled: function() {
541 var cp = this.recordClass.getMeta('containerProperty'),
542 container = this.record.data[cp],
543 grants = (container && container.hasOwnProperty('account_grants')) ? container.account_grants : null,
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);
558 * init record to edit
560 initRecord: function() {
561 Tine.log.debug('init record with mode: ' + this.mode);
563 Tine.log.debug('creating new default data record');
564 this.record = new this.recordClass(this.recordClass.getDefaultData(), 0);
567 if (this.mode !== 'local') {
568 if (this.record && this.record.id) {
569 this.loadRemoteRecord();
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});
583 * load record via record proxy
585 loadRemoteRecord: function() {
586 Tine.log.info('initiating record load via proxy');
587 this.loadRequest = this.recordProxy.loadRecord(this.record, {
589 success: function(record) {
590 this.record = record;
597 * copy this.record record
599 doCopyRecord: function() {
600 this.record = this.doCopyRecordToReturn(this.record);
604 * Copy record and returns "new record with same settings"
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']);
613 var fieldsToCopy = this.recordClass.getFieldNames().diff(omitFields),
614 recordData = Ext.copyTo({}, record.data, fieldsToCopy);
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']
621 var setProperties = {alarms: {sent_status: 'pending'}};
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++) {
629 r[index][prop] = null;
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;
649 return new this.recordClass(recordData, 0);
654 * executed after record got updated from proxy
656 onRecordLoad: function() {
657 // interrupt process flow until dialog is rendered
658 if (! this.rendered) {
659 this.onRecordLoad.defer(250, this);
662 Tine.log.debug('Tine.widgets.dialog.EditDialog::onRecordLoad() - Loading of the following record completed:');
663 Tine.log.debug(this.record);
665 if (this.copyRecord) {
667 this.window.setTitle(String.format(_('Copy {0}'), this.i18nRecordName));
669 if (! this.record.id) {
670 this.window.setTitle(String.format(_('Add New {0}'), this.i18nRecordName));
672 this.window.setTitle(String.format(_('Edit {0} "{1}"'), this.i18nRecordName, this.record.getTitle()));
676 var ticketFn = this.onAfterRecordLoad.deferByTickets(this),
677 wrapTicket = ticketFn();
679 this.fireEvent('load', this, this.record, ticketFn);
683 // finally load the record into the form
684 onAfterRecordLoad: function() {
685 var form = this.getForm();
688 form.loadRecord(this.record);
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'));
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());
702 this.loadMask.hide();
707 * executed when record gets updated from form
709 onRecordUpdate: function() {
710 var form = this.getForm();
712 // merge changes from form into record
713 form.updateRecord(this.record);
719 onRender : function(ct, position){
720 Tine.widgets.dialog.EditDialog.superclass.onRender.call(this, ct, position);
722 // generalized keybord map for edit dlgs
723 new Ext.KeyMap(this.el, [
725 key: [10,13], // ctrl + return
729 if (this.getForm().hasOwnProperty('items')) {
730 // force set last selected field
731 this.getForm().items.each(function(item) {
733 item.setValue(item.getRawValue());
737 this.action_saveAndClose.execute();
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();
749 * update (action updateer) top and bottom toolbars
751 updateToolbars: function(record, containerField) {
752 if (! this.evalGrants) {
757 this.action_saveAndClose,
758 this.action_applyChanges,
762 Tine.widgets.actionUpdater(record, actions, containerField);
763 Tine.widgets.actionUpdater(record, this.tbarItems, containerField);
769 getToolbar: function() {
770 return this.getTopToolbar();
778 isValid: function() {
779 return this.getForm().isValid();
783 * vaidates on multiple edit
787 isMultipleValid: function() {
794 onCancel: function(){
795 this.fireEvent('cancel');
796 this.purgeListeners();
803 onSaveAndClose: function() {
804 this.fireEvent('saveAndClose');
805 this.onApplyChanges(true);
809 * generic apply changes handler
810 * @param {Boolean} closeWindow
812 onApplyChanges: function(closeWindow) {
818 this.loadMask.show();
820 var ticketFn = this.doApplyChanges.deferByTickets(this, [closeWindow]),
821 wrapTicket = ticketFn();
823 this.fireEvent('save', this, this.record, ticketFn);
828 * is called from onApplyChanges
829 * @param {Boolean} closeWindow
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();
837 this.copyRecord = false;
839 if (this.isValid()) {
840 if (this.mode !== 'local') {
841 this.recordProxy.saveRecord(this.record, {
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
853 var ticketFn = this.onAfterApplyChanges.deferByTickets(this, [closeWindow]),
854 wrapTicket = ticketFn();
856 this.fireEvent('update', Ext.util.JSON.encode(this.record.data), this.mode, this, ticketFn);
859 failure: this.onRequestFailed,
860 timeout: 300000 // 5 minutes
862 duplicateCheck: this.doDuplicateCheck
866 var ticketFn = this.onAfterApplyChanges.deferByTickets(this, [closeWindow]),
867 wrapTicket = ticketFn();
869 this.fireEvent('update', Ext.util.JSON.encode(this.record.data), this.mode, this, ticketFn);
874 this.loadMask.hide();
875 Ext.MessageBox.alert(_('Errors'), this.getValidationErrorMessage());
879 onAfterApplyChanges: function(closeWindow) {
880 this.window.rename(this.windowNamePrefix + this.record.id);
881 this.loadMask.hide();
885 this.window.fireEvent('saveAndClose');
886 this.purgeListeners();
892 * get validation error message
896 getValidationErrorMessage: function() {
897 return _('Please fix the errors noted.');
901 * generic delete handler
903 onDelete: function(btn, e) {
904 Ext.MessageBox.confirm(_('Confirm'), String.format(_('Do you really want to delete this {0}?'), this.i18nRecordName), function(_button) {
906 var deleteMask = new Ext.LoadMask(this.getEl(), {msg: String.format(_('Deleting {0}'), this.i18nRecordName)});
909 this.recordProxy.deleteRecords(this.record, {
911 success: function() {
912 this.purgeListeners();
915 failure: function () {
916 Ext.MessageBox.alert(_('Failed'), String.format(_('Could not delete {0}.'), this.i18nRecordName));
917 Ext.MessageBox.hide();
925 * duplicate(s) found exception handler
927 * @param {Object} exception
929 onDuplicateException: function(exception) {
930 var resolveGridPanel = new Tine.widgets.dialog.DuplicateResolveGridPanel({
932 store: new Tine.widgets.dialog.DuplicateResolveStore({
934 recordClass: this.recordClass,
935 recordProxy: this.recordProxy,
937 clientRecord: exception.clientRecord,
938 duplicates: exception.duplicates
944 this.action_saveAndClose
948 // intercept save handler
949 resolveGridPanel.btnSaveAndClose.setHandler(function(btn, e) {
950 var resolveStrategy = resolveGridPanel.store.resolveStrategy;
952 // action discard -> close window
953 if (resolveStrategy == 'discard') {
954 return this.onCancel();
957 this.record = resolveGridPanel.store.getResolvedRecord();
959 // quit copy mode before populating form with resolved data
960 this.copyRecord = false;
963 mainCardPanel.layout.setActiveItem(this.id);
964 resolveGridPanel.doLayout();
966 this.doDuplicateCheck = false;
967 this.onSaveAndClose();
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();
979 * generic request exception handler
981 * @param {Object} exception
983 onRequestFailed: function(exception) {
986 if (exception.code == 629) {
987 this.onDuplicateException.apply(this, arguments);
989 Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
991 this.loadMask.hide();
995 * creates the relations panel, if relations are defined
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 });
1003 // interrupt process flow until dialog is rendered
1004 if (! this.rendered) {
1005 this.initRelationsPanel.defer(250, this);
1008 // add relations panel if this is rendered
1009 if (this.items.items[0]) {
1010 this.items.items[0].add(this.relationsPanel);
1013 Tine.log.debug('Tine.widgets.dialog.EditDialog::initRelationsPanel() - Initialized relations panel and added to dialog tab items.');
1018 * creates attachments panel
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);