Ext.namespace('Zarafa.core.plugins'); /** * @class Zarafa.core.plugins.RecordComponentPlugin * @extends Object * @ptype zarafa.recordcomponentplugin * * This plugin is used on a root {@link Ext.Container container} in which multiple children are * located which share a {@link Zarafa.core.data.MAPIRecord record} for displaying or editing. * This can for example occur in a {@link Ext.Window Dialog} which is used for editing a message, * or in the {@link Zarafa.core.ui.PreviewPanel PreviewPanel} for displaying a message. * * If the {@link #allowWrite} option is enabled, the {@link #record} will be editable, this will be done * by copying the {@link #record} into the {@link Zarafa.core.data.ShadowStore ShadowStore} * from where it can be saved to the server. * * The components located in the root {@link Ext.Container container} can use the * {@link Zarafa.core.ui.RecordComponentUpdaterPlugin RecordComponentUpdaterPlugin} for handling the notifications * from this plugin. * * When this plugin is being installed, it will set a reference to itself on the {@link #field} * with the name 'recordComponentPlugin'. */ Zarafa.core.plugins.RecordComponentPlugin = Ext.extend(Object, { /** * The contained on which this plugin has been installed. * @property * @type Ext.Container */ field : undefined, /** * @cfg {Boolean} ignoreUpdates True if the {@link #field} should not be listening to changes * made to the {@link #record} from a {@link Zarafa.core.data.MAPIStore Store}. This will force * the {@link #useShadowStore} option to be enabled. */ ignoreUpdates : false, /** * @cfg {Boolean} allowWrite True if the {@link #field} supports * the editing and saving of the {@link #record}. */ allowWrite : false, /** * True when the {@link #field} has been layed out. * @property * @type Boolean */ isLayoutCalled : false, /** * The record which is currently displayed in {@link #field}. * @property * @type Zarafa.core.data.MAPIRecord */ record : undefined, /** * The cheapCopy argument of {@link #setRecord}, this is used when {@link #setRecord} was * called when the panel has not been rendered yet and the call is deferred. * @property * @type Boolean */ cheapCopy : undefined, /** * @cfg {Boolean} useShadowStore True, if the {@link #record} should be pushed into the * shadowStore. */ useShadowStore : false, /** * Indicates if the {@link #record} has been changed by the user since it has been loaded. * If this is changed to true for the first time, the {@link #userupdaterecord} event will be * fired on the {@link #field}. * @property * @type Boolean * @private */ isChangedByUser : false, /** * @cfg {Array} loadTasks An array of objects containing tasks which should be executed * in order to properly load all data into the Component. The object should contain the * 'fn' field which contains the function to be called, this will be called with the following * arguments: * - container {@link Ext.Container} The container on which this plugin is installed * - record {@link Zarafa.core.data.MAPIRecord} The record which is being loaded * - task {@link Object} The task object which was registered * - callback {@link Function} The callback function which should be called when the task * has completed its work. * The task should also contain the 'scope' property which refers to the scope in which the * 'fn' should be called. If the task contains the 'defer' property, the call to 'fn' will * be defferred for the given number of milliseconds. */ loadTasks : undefined, /** * @cfg {Boolean} enableOpenLoadTask True to create a {@link #loadTask} which will * {@link Zarafa.core.data.MAPIRecord#open open} the {@link #record} when it is set * on the {@link #field}. */ enableOpenLoadTask : true, /** * @cfg {Number} autoOpenLoadTaskDefer The 'defer' configuration option for the task * which will be put in {@link #loadTasks} when {@link #enableOpenLoadTask} is true. * This can be used to delay the opening of the record. */ autoOpenLoadTaskDefer : 0, /** * List of {@link Ext.util.DelayedTask} instances which will be filled in when * tasks in {@link #loadTasks} was configured with a 'defer' property. * @property * @type Array * @private */ scheduledTasks : undefined, /** * The list of Tasks from {@link #loadTasks} which have been started but are awaiting * the call to the callback function. * @property * @type Array * @private */ pendingTasks : undefined, /** * @constructor * @param {Object} config Configuration object */ constructor : function(config) { config = config || {}; // Apply a default value for the useShadowStore // based on the other settings. if (!Ext.isDefined(config.useShadowStore)) { if (config.allowWrite === true) { config.useShadowStore = true; } if (config.ignoreUpdates === true) { config.useShadowStore = true; } } Ext.apply(this, config); }, /** * Initializes the {@link Ext.Component Component} to which this plugin has been hooked. * @param {Ext.Component} The parent field to which this component is connected */ init : function(field) { this.field = field; field.recordComponentPlugin = this; this.field.addEvents( /** * @event beforesetrecord * * Fired from {@link #field} * * Fires when a record is about to be added to the {@link #field} * This will allow Subclasses modify the {@link Zarafa.core.data.MAPIRecord record} before * it has been hooked into the {@link #field}. * @param {Ext.Container} panel The panel to which the record was set * @param {Zarafa.core.data.MAPIRecord} record The record which is going to be set * @param {Zarafa.core.data.MAPIRecord} oldrecord The oldrecord which is currently set * @return {Boolean} false if the record should not be set on this panel. */ 'beforesetrecord', /** * @event setrecord * * Fired from {@link #field} * * Fires when a record has been added to the {@link #field}. * No event handler may modify any properties inside the provided record (if this * is needed for the Panel initialization, use the {@link #beforesetrecord} event). * @param {Ext.Container} panel The panel to which the record was set * @param {Zarafa.core.data.MAPIRecord} record The record which was set * @param {Zarafa.core.data.MAPIRecord} oldrecord The oldrecord which was previously set */ 'setrecord', /** * @event beforeloadrecord * * Fired from {@link #field} * * Fires when a record is going to be {@link Zarafa.core.data.MAPIRecord#isOpened opened}, * or if there are registered {@link #loadTasks} to be executed. * No event handler may modify any properties inside the provided record. * * @param {Ext.Container} panel The panel to which the record was set * @param {Zarafa.core.data.MAPIRecord} record The record which was updated * @return {Boolean} false if the record should not be loaded. */ 'beforeloadrecord', /** * @event loadrecord * * Fired from {@link #field} * * Fires when a record has been {@link Zarafa.core.data.MAPIRecord#isOpened opened}, * and all registered {@link #loadTasks} have been executed. * No event handler may modify any properties inside the provided record. * * @param {Ext.Container} panel The panel to which the record was set * @param {Zarafa.core.data.MAPIRecord} record The record which was updated */ 'loadrecord', /** * @event updaterecord * * Fired from {@link #field} * * Fires when the record of this {@link #field} was updated, * through a {@link Zarafa.core.data.MAPIStore store}. * No event handler may modify any properties inside the provided record. * @param {Ext.Container} panel The panel to which the record was set * @param {String} action write Action that ocurred. Can be one of * {@link Ext.data.Record.EDIT EDIT}, {@link Ext.data.Record.REJECT REJECT} or * {@link Ext.data.Record.COMMIT COMMIT} * @param {Zarafa.core.data.MAPIRecord} record The record which was updated */ 'updaterecord', /** * @event userupdaterecord * * Fired from {@link #field} * * Fires when the record of this {@link #field} was changed by the user. * This allows the UI to apply special indicators to show the user that * the user might need to save his changes. * @param {Ext.Container} panel The panel to which the record was set * @param {Zarafa.core.data.MAPIRecord} record The record which is dirty */ 'userupdaterecord', /** * @event exceptionrecord * * Fired from {@link #field} * * Fires when an exception event was fired by the {@link Ext.data.DataProxy}. * No event handler may modify any properties inside the provided record. * @param {String} type See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {String} action See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Object} options See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Object} response See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Zarafa.core.data.MAPIRecord} record The record which was subject of the request * that encountered an exception. * @param {String} error (Optional) Passed when a thrown JS Exception or JS Error is * available. */ 'exceptionrecord', /** * @event writerecord * * Fired from {@link #field} * * Fires when write event was fired by the {@link Zarafa.core.data.MAPIStore store}. * @param {Ext.data.Store} store * @param {String} action [Ext.data.Api.actions.create|update|destroy] * @param {Object} result The 'data' picked-out out of the response for convenience. * @param {Ext.Direct.Transaction} res * @param {Zarafa.core.data.IPMRecord[]} records Store's records, the subject(s) of the write-action */ 'writerecord' ); // Raise an event after all components have been layout, we can then call this.setRecord // to force all components to be aware of the Record which is bound to this component. // we can't do that in the constructor because then nobody is listening to the event // yet. Neither can we do it after rendering, since that only indicates that this container. // has been rendered and not the components. this.field.on('afterlayout', this.onAfterFirstLayout, this, {single : true}); // Add event listener for the 'close' event, if we are editing the record, then the // record must be removed from the shadowStore when closing the dialog. this.field.on('close', this.onClose, this); }, /** * Start editing on a {@link Zarafa.core.data.MAPIRecord record} by hooking * it into the {@link Zarafa.core.data.ShadowStore ShadowStore} and mark the * {@link Zarafa.core.data.MAPIRecord record} as being edited. This function * should only be called when {@link #useShadowStore} is set to true. * * @param {Zarafa.core.data.MAPIRecord} record The record to edit * @private */ startShadowRecordEdit : function(record) { if(this.useShadowStore) { container.getShadowStore().add(record); } }, /** * Stop editing on a {@link Zarafa.core.data.MAPIRecord record} by unhooking it * from the {@link Zarafa.core.data.ShadowStore ShadowStore}. This function * should only be called when {@link #useShadowStore} is set to true. * * @param {Zarafa.core.data.MAPIRecord} record The record to finish editing * @private */ endShadowRecordEdit : function(record) { if(this.useShadowStore) { container.getShadowStore().remove(record, true); } }, /** * Check if the given {@link Zarafa.core.data.MAPIRecord record} matches the {@link #record} * which is used inside this container. When we have {@link #allowWrite} support, we only accept updates * when the record is exactly the same as the record inside the container. But when we have * read-only support, we accept updates from all records with the same entryid. * * @param {Zarafa.core.data.MAPIRecord} record The record to compare * @return {Boolean} True if the given record matches the record inside this container * @private */ isRecordInContainer : function(record) { if (this.allowWrite === true && this.useShadowStore === false) { return (this.record && this.record === record); } else { return (this.record && this.record.equals(record)); } }, /** * Set the {@link Zarafa.core.data.MAPIRecord record} which must be shown inside the {@link #field} * When the field has not yet {@link #isLayoutCalled layed out}, then the {@link #record} is * set, but all work is deferred to the first {@link #doLayout layout} action on this container. * Otherwise this function will call {@link #beforesetrecord} to check if the record should be * set on this field. Depending on the {@link Zarafa.core.data.MAPIRecord#isOpened opened} status * of the record, it will call to the server to open the record (and wait with displaying the * record until the server has responsed) or display the record directly. * @param {Zarafa.core.data.MAPIRecord} record The record to set * @param {Boolean} cheapCopy true to prevent the record from being copied. This is usually the case * when {@link #allowWrite} is enabled, and the given record is not a phantom. */ setRecord : function(record, cheapCopy) { var oldrecord = this.record; if (this.record === record) { return; } // Cancel any scheduled tasks which // might have been registered for // the previous record. if (this.scheduledTasks) { Ext.each(this.scheduledTasks, function(task) { task.cancel(); }); this.scheduledTasks = []; } // Clear all pending tasks for the // previous registered record. if (this.pendingTasks) { this.pendingTasks = []; } // Layout has not yet been called, cache the record // until the onAfterFirstLayout has been called, which will // set the record again. if (this.isLayoutCalled === false) { this.record = record; this.cheapCopy = cheapCopy; return; } if (this.field.fireEvent('beforesetrecord', this, record, oldrecord) === false) { return; } // Unhook the old record from the modifications tracking if (oldrecord) { oldrecord.setUpdateModificationsTracking(false); if (this.ignoreUpdates !== true) { this.field.mun(oldrecord.getStore(), 'update', this.onUpdateRecord, this); this.field.mun(oldrecord.getStore(), 'write', this.onWrite, this); this.field.mun(oldrecord.getStore(), 'exception', this.onExceptionRecord, this); } if (this.useShadowStore === true) { this.endShadowRecordEdit(oldrecord); } } // If we are clearing the record, we will take a shortcut, simply fire the // 'setrecord' event. if (!record) { this.record = record; this.field.fireEvent('setrecord', this, record, oldrecord); return; } // Check if the record must be move into the ShadowStore. When the record // is a phantom we can safely move it, otherwise we need to create a copy. // If the cheapCopy argument was provided, then it means we have already // received a copy and we can move it without problems. if (this.useShadowStore) { if (record.phantom !== true && cheapCopy !== true) { record = record.copy(); } this.startShadowRecordEdit(record); } // All preparations have been completed, it is now time to set // the Record correctly into the component. this.record = record; // remove modified from modal dialog record if(this.record.isModalDialogRecord) { delete this.record.modified; } this.record.setUpdateModificationsTracking(true); this.field.fireEvent('setrecord', this, record, oldrecord); // Register the exception event handler, as we want to catch exceptions for // opening the record as well. if (this.ignoreUpdates !== true) { this.field.mon(record.getStore(), 'exception', this.onExceptionRecord, this); } // If the record is not yet opened, we must open the record now, // Add the 'openTaskHandler' to the loadTasks so we can start loading it. var tasks = this.loadTasks ? this.loadTasks.clone() : []; if (!record.isOpened() && this.enableOpenLoadTask) { tasks.push({ fn : this.openTaskHandler, scope : this, defer : this.autoOpenLoadTaskDefer }); } // Start loading the record. this.doLoadRecord(record, tasks); }, /** * Task handler for {@link #loadTasks} in case the {@link #record} needs * to be opened. This will open the record, and wait for the response from * the server. * @param {Ext.Component} component The component which contains the record * @param {Zarafa.core.data.MAPIRecord} record The record to be opened * @param {Object} task The task object * @parma {Function} callback The function to call when the task has been * completed. * @private */ openTaskHandler : function(component, record, task, callback) { var fn = function(store, record) { if (this.isRecordInContainer(record)) { this.field.mun(store, 'open', fn, this); if (this.record !== record) { this.record.applyData(record); } callback(); } }; this.field.mon(record.getStore(), 'open', fn, this); record.open(); }, /** * Obtain the current {@link #record} which has been sent to all * listeners of the {@link #setrecord} event. * * @return {Zarafa.core.data.MAPIRecord} The record which is currently active */ getActiveRecord : function() { // The setrecord event is called during setRecord, but always after // isLayoutCalled. if (this.isLayoutCalled === true) { return this.record; } }, /** * Event handler which is triggered after the {@link #field} * is first time layed out. This will reset the current {@link #record} * to trigger the {@link #setrecord} event for the initial Record. * * @param {Ext.Component} component This component * @param {ContainerLayout} layout The layout * @private */ onAfterFirstLayout : function(component, layout) { this.isLayoutCalled = true; if (this.record) { // Force record reload by clearing this.record first. var record = this.record; this.record = undefined; this.setRecord(record, this.cheapCopy); } }, /** * This will check if any {@link #loadTasks tasks} have been registered. * If there are, this will fire the {@link #beforeloadrecord} event, and * starts executing the tasks. When it is done, the {@link #loadrecord} event * is fired. * Finally in all cases, the function {@link #afterLoadRecord} is called * to complete the loading of the record. * @param {Zarafa.core.data.MAPIRecord} record The record to load * @param {Array} Array of Task objects as copied from {@link this.loadTasks}. * @private */ doLoadRecord : function(record, tasks) { this.pendingTasks = []; this.scheduledTasks = []; // Check if we have any loading tasks, if we do, then start executing // them and wait for all of them to complete before continuing // to afterLoadRecord. if (!Ext.isEmpty(tasks)) { if (this.field.fireEvent('beforeloadrecord', this.field, record) !== false) { Ext.each(tasks, function(task) { // Add it to the pendingTasks list, so we can // keep track of the pending completion events. this.pendingTasks.push(task); // Check if the task should be deferred, if so // then use a Ext.util.DelayedTask to schedule it. if (task.defer && task.defer > 0) { var delayTask = new Ext.util.DelayedTask(this.doTask, this, [ record, task ]); // Add it to the scheduledTasks, so we can // keep track of the tasks (and cancel them // if needed). this.scheduledTasks.push(delayTask); delayTask.delay(task.defer); } else { this.doTask(record, task); } }, this); } } else { this.afterLoadRecord(record); } }, /** * Execute a task as registerd in the {@link #loadTasks registered tasks}. * @param {Zarafa.core.data.MAPIRecord} record The record to perform the action on * @param {Object} task The task object from {@link #loadTasks} * @private */ doTask : function(record, task) { if (this.isRecordInContainer(record)) { task.fn.call(task.scope || task, this.field, record, task, this.doLoadRecordCallback.createDelegate(this, [ task ])); } }, /** * Callback function used when executing {@link #loadTasks tasks}. Each * time this function is called, the task is removed from the {@link #loadTasks} * list. If this was the last task, {@link #loadrecord} event is fired, and * {@link #afterLoadRecord} will be called. * @param {Object} task The task which was completed * @private */ doLoadRecordCallback : function(task) { var record = this.record; // Unregister the task when it is completed. this.pendingTasks.remove(task); // Check if this was the last task... if (this.pendingTasks.length === 0) { // If all pending Tasks were done, all // scheduled tasks have been completed as well this.scheduledTasks = []; this.field.fireEvent('loadrecord', this.field, record); this.afterLoadRecord(record); } }, /** * Reset the {@link #isChangedByUser} property to false and fire the {@link #userupdaterecord} event * to signal the value change. This will tell all components that the {@link #record} contains * no modifications from the user. * @private */ resetUserChangeTracker : function() { this.isChangedByUser = false; this.field.fireEvent('userupdaterecord', this.field, this.record, this.isChangedByUser); }, /** * Set the {@link #isChangedByUser} property to true and fire the {@link #userupdaterecord} event * when this means the property was changed. This will tell all components that the {@link #record} * contains modifications from the user. * @private */ registerUserChange: function() { if (this.isChangedByUser !== true) { this.isChangedByUser = true; this.field.fireEvent('userupdaterecord', this.field, this.record, this.isChangedByUser); } }, /** * Check if the given record contains user changes. This will check the contents for * {@link Zarafa.core.data.MAPIRecord#updateModifications updateModifications} and * {@link Zarafa.core.data.MAPIRecord#updateSubStoreModifications updateSubStoreModifications} * to see if they were modified. * @param {Zarafa.core.data.MAPIRecord} record The record to validate * @private */ checkForUserChanges : function(record) { var updateModifications = record.updateModifications; var updateSubStoreModifications = record.updateSubStoreModifications; if ((updateModifications && Object.keys(updateModifications).length > 0) || (updateSubStoreModifications && Object.keys(updateSubStoreModifications).length > 0)) { this.registerUserChange(); } }, /** * Called by {@link #doLoadRecord} when all {@link #loadTasks} have been executed * (or when no tasks were registered}. * * This will register additional event handlers on the {@link Zarafa.core.data.MAPIStore store}. * * @param {Zarafa.core.data.MAPIRecord} record The record which has been loaded * @private */ afterLoadRecord : function(record) { if (this.ignoreUpdates !== true) { this.resetUserChangeTracker(); var store = this.record.getStore(); this.field.mon(store, { 'update' : this.onUpdateRecord, 'write' : this.onWrite, 'scope' : this }); } }, /** * Event handler will be called when the {@link Zarafa.core.data.MAPIStore Store} has * updated the {@link Zarafa.core.data.MAPIRecord record}. This function will fire * {@link #updaterecord} event to notify that the record is updated. * * @param {Zarafa.core.data.IPMStore} store The store in which the record is located. * @param {Zarafa.core.data.IPMRecord} record The record which has been updated * @param {String} operation The update operation being performed. Value may be one of * {@link Ext.data.Record.EDIT EDIT}, {@link Ext.data.Record.REJECT REJECT} or * {@link Ext.data.Record.COMMIT COMMIT} * @private */ onUpdateRecord : function(store, record, operation) { if (!this.isRecordInContainer(record)) { return; } // Check if this exact record was updated, or if this is an external notification. // If this was a notification, then we only accept committed changes, before applying // the update to our current active record. if (this.record !== record) { if (operation !== Ext.data.Record.COMMIT) { return; } this.record.applyData(record); } if (this.ignoreUpdates !== true) { if (operation === Ext.data.Record.COMMIT) { this.resetUserChangeTracker(); } else { this.checkForUserChanges(record); } } this.field.fireEvent('updaterecord', this.field, operation, this.record); }, /** * Event handler will be called when the {@link Zarafa.core.data.MAPIStore Store} has * fired an exception event. * @param {Ext.data.DataProxy} proxy The proxy which fired the event. * No event handler may modify any properties inside the provided record. * @param {String} type See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {String} action See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Object} options See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Object} response See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} * for description. * @param {Zarafa.core.data.MAPIRecord} record The record which was subject of the request * that encountered an exception. * @param {String} error (Optional) Passed when a thrown JS Exception or JS Error is * available. * @private */ onExceptionRecord : function(proxy, type, action, options, response, args) { /** Extract the record from the args, and pull it out of an array if that is the case. * If the array length is bigger than 1, we can discard this exception as this * RecordCompontentPlugin can only handle one at a time. In that case the record variable * will remain an array and will not pass the next IF-statement when it is compared against * the MAPIRecord class. */ var record = args.sendRecords; if(Array.isArray(record) && record.length == 1){ record = record[0]; } // If it is not a record or if the record is not the record in the Container we should skip it if(!(record instanceof Zarafa.core.data.MAPIRecord) || !this.isRecordInContainer(record)){ return; } var error = args.error || undefined; this.field.fireEvent('exceptionrecord', type, action, options, response, record, error); }, /** * Event handler will be called when the {@link Zarafa.core.data.MAPIStore Store} has * fired a write event. * Store fires it if the server returns 200 after an Ext.data.Api.actions CRUD action. * * @param {Ext.data.Store} store * @param {String} action [Ext.data.Api.actions.create|update|destroy] * @param {Object} result The 'data' picked-out out of the response for convenience. * @param {Ext.Direct.Transaction} res * @param {Zarafa.core.data.IPMRecord[]} records Store's records, the subject(s) of the write-action * @private */ onWrite : function(store, action, result, res, records) { // If records isn't array then make it. records = [].concat(records); for (var i = 0, len = records.length; i < len; i++) { if (this.record === records[i]) { if(action == Ext.data.Api.actions.destroy) { // the record has been destroyed and removed from store // so user made changes are not usefull anymore this.resetUserChangeTracker(); } this.field.fireEvent('writerecord', store, action, result, res, records[i]); return; } } }, /** * Event handler which is called when the container is being closed. * This will remove the {@link Zarafa.core.data.MAPIRecord record} from the * {@link Zarafa.core.data.ShadowStore ShadowStore} (when the record was being {@link #allowWrite edited}). * @param {Ext.Component} container The parent container on which this plugin is installed. * @private */ onClose : function(dialog) { if (this.record) { this.endShadowRecordEdit(this.record); } } }); Ext.preg('zarafa.recordcomponentplugin', Zarafa.core.plugins.RecordComponentPlugin);