/* * Tine 2.0 * * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 * @author Cornelius Weiss * @copyright Copyright (c) 2011-2012 Metaways Infosystems GmbH (http://www.metaways.de) */ Ext.ns('Tine.Calendar'); /** * display panel for MIME type text/calendar * * NOTE: this panel is registered on Tine.Calendar::init * * @namespace Tine.Calendar * @class Tine.Calendar.iMIPDetailsPanel * @author Cornelius Weiss * @constructor */ Tine.Calendar.iMIPDetailsPanel = Ext.extend(Tine.Calendar.EventDetailsPanel, { /** * @cfg {Object} preparedPart * server prepared text/calendar iMIP part */ preparedPart: null, /** * @property actionToolbar * @type Ext.Toolbar */ actionToolbar: null, /** * @property iMIPrecord * @type Tine.Calendar.Model.iMIP */ iMIPrecord: null, allowViewInCalendar: true, /** * init this component */ initComponent: function() { this.app = Tine.Tinebase.appMgr.get('Calendar'); this.resolveIMIP(this.messageRecord.get('preparedParts')[0].preparedData); this.initIMIPToolbar(); this.on('afterrender', async (cmp, ownerCt) => { const attendeeList = this.getAttendeeList(); await this.attendeeCombo.syncStore(attendeeList); this.showIMIP(); }); this.on('updateEvent', async (result) => { this.resolveIMIP(result); this.showIMIP(); }); Tine.Calendar.iMIPDetailsPanel.superclass.initComponent.call(this); }, resolveIMIP(data) { if (data) { this.prepareData = data; this.iMIPrecord = new Tine.Calendar.Model.iMIP(this.prepareData); if (!this.iMIPrecord.get('event')) { this.iMIPrecord.set('event', this.prepareData.event); } } this.hasExistingEvent = !!this.iMIPrecord.get('existing_event'); const preconditions = this.iMIPrecord.get('preconditions'); const eventData = (this.hasExistingEvent && !preconditions) ? this.iMIPrecord.get('existing_event') : this.iMIPrecord.get('event'); this.record = Tine.Calendar.backend.recordReader({ responseText: Ext.util.JSON.encode(eventData) }); // show container from existing event if exists const containerId = this.iMIPrecord.get('existing_event')?.container_id; if (this.hasExistingEvent && containerId) { this.record.set('container_id', containerId); } }, /** * process IMIP * * @param {String} status */ processIMIP: async function (status, range) { if (this.record.isRecurBase() && status !== 'ACCEPTED' && !range) { Tine.widgets.dialog.MultiOptionsDialog.openWindow({ title: this.app.i18n._('Reply to Recurring Event'), questionText: this.app.i18n._('You are responding to a recurring event. What would you like to do?'), height: 170, scope: this, options: [ {text: this.app.i18n._('Respond to the whole series'), name: 'series'}, {text: this.app.i18n._('Do not respond'), name: 'cancel'} ], handler: function (option) { if (option !== 'cancel') { this.processIMIP(status, option); } } }); return; } if (!this.targetAttendeeRecord) return; const attendeeContainers = this.iMIPrecord.get('attendeeContainersAvailable'); const userId = this.targetAttendeeRecord.data.user_id?.id ?? this.targetAttendeeRecord.data.user_id; const options = Object.entries(attendeeContainers) .filter(([key]) => key.includes(userId)) .flatMap(([, records]) => records.map(record => ({ text: record.name, name: record }))); if (options.length === 0) return; const processUpdate = (selectedOption) => { const targetAttendeeData = {...this.targetAttendeeRecord.data}; if (selectedOption) { targetAttendeeData.displaycontainer_id = selectedOption; } targetAttendeeData.status = status; this.getLoadMask().show(); Tine.Calendar.iMIPProcess(this.iMIPrecord.data, targetAttendeeData, (result, response) => { if (!response.error) { this.targetAttendeeRecord.set('status', status); this.targetAttendeeRecord.set('displaycontainer_id', selectedOption); } this.fireEvent('updateEvent', result); }, this); }; if (this.targetAttendeeRecord.get('status') !== 'NEEDS-ACTION' || options.length === 1) { return processUpdate(); } Tine.widgets.dialog.MultiOptionsDialog.openWindow({ title: this.app.i18n._('Update Status'), questionText: this.app.i18n._('Please select a calendar before update your status'), height: 170, allowCancel: true, scope: this, options: options, handler: function(option) { if (option !== 'cancel') { processUpdate(option); } else { this.updateInfo(); } } }); }, /** * iMIP action toolbar */ initIMIPToolbar: function() { this.viewInCalendarAction = new Ext.Button(new Ext.Action({ handler: () => { Tine.Calendar.ViewInCalendarDialog.openWindow({ record: this.record, messageRecord: this.messageRecord, targetAttendeeRecord: this.targetAttendeeRecord, allowViewInCalendar: false, }); }, icon: 'images/icon-set/icon_calendar.svg', tooltip: this.app.i18n._('View and Edit Event In Calendar'), hidden: !this.allowViewInCalendar, style: "margin: 0 10px;", })); // Create the static text display with hover functionality this.attendeeTextContainer = new Ext.Container({ hidden: true, cls: 'attendee-text-container', listeners: { scope: this, render: function(c) { c.getEl().on({ scope: this, click: ()=> { this.activateCombo(); }, }); } }, items: [ this.attendeeLabel = new Ext.Component({ autoEl: { tag: 'div', cls: 'xtb-text', html: 'Select Attendee' } }), ] }); // Create the combo (initially hidden) this.attendeeCombo = new Tine.Calendar.AttendeeCombo({ eventRecord: this.record, messageRecord: this.messageRecord, defaultValue: 'current', width: 300, listeners: { scope: this, select: (combo, record)=> { this.updateInfo(); }, } }); // Create the static text display with hover functionality this.attendeeComboContainer = new Ext.Container({ hidden: true, cls: 'felamimail-action-invitation', items: [this.attendeeCombo] }); // Create a container that will hold both the text and combo this.idPrefix = Ext.id(); this.statusActions = []; this.statuses = Tine.Tinebase.widgets.keyfield.StoreMgr.get('Calendar', 'attendeeStatus'); this.statuses.each((status) => { this.statusActions.push(new Ext.Button(new Ext.Action({ id: this.idPrefix + '-tglbtn-' + status.id, text: status.id === 'NEEDS-ACTION' ? this.app.i18n._('Save') : status.get('i18nValue'), xtype: 'tbbtnlockedtoggle', enableToggle: true, handler: this.processIMIP.createDelegate(this, [status.id]), icon: status.get('icon'), hidden: true, toggleGroup: 'attendee-status-tglgroup', cls: 'felamimail-action-invitation', tooltip: status.id === 'NEEDS-ACTION' ? this.app.i18n._('Save to calendar without responding') : status.get('i18nValue'), }))); }); // add more actions here (no spam / apply / crush / send event / ...) this.iMIPclause = new Ext.Toolbar.TextItem({ text: '', style: "margin: 5px 10px;", }); this.descriptionField = new Ext.form.VueAlert({ variant: 'warning', hidden: true, }); this.tbar = this.actionToolbar = new Ext.Panel({ layout: 'fit', border: false, items: [ this.descriptionField, { xtype: 'container', cls: 'cal-event-response-panel', items: [ this.iMIPclause, { xtype: 'container', cls: 'cal-event-response-panel-group-right', items: [ { xtype: 'container', cls: 'subgroup', items: [ this.attendeeTextContainer, this.attendeeComboContainer, this.viewInCalendarAction ], }, { xtype: 'container', cls: 'subgroup', items: this.statusActions, } ] } ] }, ] }); }, activateCombo() { this.attendeeTextContainer.hide(); this.attendeeComboContainer.show(); this.attendeeComboContainer.focus(); this.attendeeCombo.onTriggerClick(); }, deactivateCombo() { this.attendeeComboContainer.hide(); this.attendeeTextContainer.show(); if (this.targetAttendeeRecord) { const prefix = this.targetAttendeeRecord.get('status') === 'NEEDS-ACTION' ? this.app.i18n._('Set response for') + ':' : ''; const attendeeName = this.targetAttendeeRecord.get('displayText') || this.targetAttendeeRecord.get('user_id').n_fileas || ''; this.attendeeLabel.update(`${prefix}${attendeeName}`); } }, getTargetAttendeeRecord() { const selectedAttendeeId = this.attendeeCombo.getValue(); if (!selectedAttendeeId) return null; this.targetAttendeeRecord = this.attendeeCombo.store.getById(selectedAttendeeId); const attendeeContainers = this.iMIPrecord.get('attendeeContainersAvailable'); const attendeeList = this.getAttendeeList(); if (this.targetAttendeeRecord) { const targetAttendeeDataFromEvent = attendeeList.find((r) => { return r.id === selectedAttendeeId; }); if (targetAttendeeDataFromEvent?.displaycontainer_id) { this.targetAttendeeRecord.set('displaycontainer_id', targetAttendeeDataFromEvent.displaycontainer_id); } else { Object.entries(attendeeContainers).forEach((key) => { const user = this.targetAttendeeRecord.get('user_id'); if (user && key[0].includes(user.id)) { this.targetAttendeeRecord.set('displaycontainer_id', key[1][0]); } }) } } return this.targetAttendeeRecord; }, getAttendeeList() { if (this.record.get('attendee')) { return this.record.get('attendee'); } const myAttender = this.record.getMyAttenderRecord().data; if (myAttender?.user_id?.id) { myAttender.id = myAttender.user_id.id; } return myAttender ? [myAttender] : []; }, /** * show/layout iMIP panel */ showIMIP: function () { this.updateInfo(); this.getLoadMask().hide(); const singleRecordPanel = this.getSingleRecordPanel(); singleRecordPanel.setVisible(true); singleRecordPanel.setHeight(200); singleRecordPanel.loadRecord(this.record); }, updateInfo() { const preconditions = this.iMIPrecord.get('preconditions'); const method = this.iMIPrecord.get('method'); const myAttenderRecord = this.record.getMyAttenderRecord(); const attenderRecord = this.getTargetAttendeeRecord() ?? myAttenderRecord; let showActions = false; //todo: show save action only when needed let showSaveAction = !this.hasExistingEvent; let text = ''; if (myAttenderRecord) { const isInRecipientList = this.messageRecord.data['to'].find((to) => {return myAttenderRecord.get('user_id').email === to.email}); if (!isInRecipientList) { this.descriptionField.setText(this.app.i18n._("Attention! , this is an invitation for ") + this.messageRecord.data['to'][0]['name']); this.descriptionField.show(); } } // check preconditions const uid = this.record.get('recurid') || this.record.get('uid'); if (preconditions?.[uid]) { const precondition = preconditions[uid]; if (precondition.hasOwnProperty('EVENTEXISTS')) { text = this.app.i18n._("The event of this message does not exist"); } else if (precondition.hasOwnProperty('ORIGINATOR')) { // display spam box -> might be accepted by user? text = this.app.i18n._("The sender is not authorized to update the event"); } else if (precondition.hasOwnProperty('RECENT')) { text = this.app.i18n._("This message has already been processed."); } else if (precondition.hasOwnProperty('ATTENDEE')) { // party crush button? if (myAttenderRecord) { showActions = true; } text = this.app.i18n._("You are not an attendee of this event"); } else if (precondition.hasOwnProperty('NOTDELETED')) { text = this.app.i18n._("This event has been deleted by the organizer"); } else if (precondition.hasOwnProperty('ORGANIZER')) { // fixme: check precondition failed, allow it in be ? text = this.app.i18n._("You are the organizer of this event."); showSaveAction = false; } else { text = this.app.i18n._("Unsupported message"); } } // method specific text / actions if (!preconditions?.[uid]) { switch (method) { case 'REQUEST': if (attenderRecord) { showActions = true; } if (!myAttenderRecord) { // might happen in shared folders -> we might want to become a party crusher? text = this.app.i18n._("This is an event invitation for someone else."); break; } if (this.hasExistingEvent) { const event = Tine.Calendar.backend.recordReader({ responseText: Ext.util.JSON.encode(this.iMIPrecord.get('event')) }); const existingEvent = Tine.Calendar.backend.recordReader({ responseText: Ext.util.JSON.encode(this.iMIPrecord.get('existing_event')) }); if (attenderRecord && attenderRecord.get('status') !== 'NEEDS-ACTION' && (event.get('external_seq') <= existingEvent.get('external_seq') || event.get('seq') <= existingEvent.get('seq'))) { const attendeeName = (attenderRecord.get('user_id').n_fn ?? attenderRecord.get('user_id').name); text = attendeeName + ' ' + this.app.i18n._("has already replied to this event invitation."); } if (existingEvent.isRescheduled(event)) { text = this.app.i18n._('The event got rescheduled.'); } } break; case 'REPLY': // Someone replied => autoprocessing atm. text = this.app.i18n._('An invited attendee responded to the invitation.'); break; case 'CANCEL': text = this.app.i18n._('This event has been canceled.'); this.allowViewInCalendar = false; break; default: text = this.app.i18n._("Unsupported method"); this.allowViewInCalendar = false break; } } this.iMIPclause.setText(text); this.viewInCalendarAction[(!this.allowViewInCalendar || !attenderRecord) ? 'hide' : 'show'](); this.deactivateCombo(); this.actionToolbar.doLayout(); this.statuses.each((status) => { const freqBtn = Ext.getCmp(this.idPrefix + '-tglbtn-' + status.id); if (!attenderRecord) { freqBtn.hide(); } else { if (status.id === 'NEEDS-ACTION') { freqBtn[showSaveAction ? 'show': 'hide'](); } else { if (showActions) { freqBtn.show(); } freqBtn.toggle(status.id === attenderRecord.get('status')); } } }) } }); Promise.all([Tine.Tinebase.appMgr.isInitialised('Felamimail'), Tine.Tinebase.ApplicationStarter.isInitialised()]).then(() => { if (Tine.Felamimail.MimeDisplayManager) { Tine.Felamimail.MimeDisplayManager.register('text/calendar', Tine.Calendar.iMIPDetailsPanel); } })