/* * Tine 2.0 * * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 * @author Cornelius Weiss * @copyright Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de) */ /* global Ext, Tine */ Ext.ns('Tine.Calendar'); Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, { /** * @cfg {String} activeView */ activeView: 'weekSheet', startDate: new Date().clearTime(), /** * $property Object view -> startdate */ startDates: null, /** * @cfg {String} loadMaskText * i18n._('Loading events, please wait...') */ loadMaskText: 'Loading events, please wait...', /** * @cfg {Number} autoRefreshInterval (seconds) */ autoRefreshInterval: 300, /** * @cfg {Boolean} initialLoadAfterRender */ initialLoadAfterRender: true, /** * @property autoRefreshTask * @type Ext.util.DelayedTask */ autoRefreshTask: null, /** * add records from other applications using the split add button * - activated by default * * @type Bool * @property splitAddButton */ splitAddButton: true, /** * default print mode * * @type String {sheet|grid} * @property defaultPrintMode */ defaultPrintMode: 'sheet', periodRe: /^(day|week|month|year|custom)/i, presentationRe: /(sheet|grid|timeline)$/i, calendarPanels: {}, border: false, layout: 'border', stateful: true, stateId: 'cal-mainscreen', stateEvents: ['changeview'], hasQuickSearchFilterToolbarPlugin: true, getState: function () { return Ext.copyTo({}, this, 'activeView'); }, applyState: Ext.emptyFn, initComponent: function () { var me = this; this.addEvents( /** * @event changeview * fired if an event got clicked * @param {Tine.Calendar.MainScreenCenterPanel} mspanel * @param {String} view */ 'changeview'); this.recordClass = Tine.Calendar.Model.Event; this.app = Tine.Tinebase.appMgr.get('Calendar'); // init some translations this.i18nRecordName = this.app.i18n.n_hidden(this.recordClass.getMeta('recordName'), this.recordClass.getMeta('recordsName'), 1); this.i18nRecordsName = this.app.i18n._hidden(this.recordClass.getMeta('recordsName')); this.i18nContainerName = this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1); this.i18nContainersName = this.app.i18n._hidden(this.recordClass.getMeta('containersName')); this.loadMaskText = this.app.i18n._hidden(this.loadMaskText); var state = Ext.state.Manager.get(this.stateId, {}); Ext.apply(this, state); this.defaultFilters = [ {field: 'attender', operator: 'in', value: [Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), { user_type: 'user', user_id: Tine.Tinebase.registry.get('currentAccount') })]}, {field: 'attender_status', operator: 'notin', value: ['DECLINED']} ]; this.filterToolbar = new Tine.widgets.grid.FilterPanel({ app: this.app, recordClass: this.recordClass, filterModels: this.recordClass.getFilterModel(), defaultFilter: 'query', filters: this.defaultFilters || [], onFilterChange: this.refresh.createDelegate(this, [false]), getAllFilterData: this.getAllFilterData.createDelegate(this), }); this.filterToolbar.getQuickFilterPlugin().criteriaIgnores.push( {field: 'period'}, {field: 'grants'} ); this.startDates = []; this.initActions(); this.initLayout(); // init autoRefresh this.autoRefreshTask = new Ext.util.DelayedTask(this.refresh, this, [{ refresh: true, autoRefresh: true }]); this.initMessageBus(); Tine.Calendar.MainScreenCenterPanel.superclass.initComponent.call(this); }, initMessageBus: function() { this.postalSubscriptions = []; this.postalSubscriptions.push(postal.subscribe({ channel: "recordchange", topic: 'Calendar.Event.*', callback: this.onRecordChanges.createDelegate(this) })); }, initActions: function () { this.action_editInNewWindow = new Ext.Action({ requiredGrant: 'readGrant', text: this.i18nEditActionText ? this.app.i18n._hidden(this.i18nEditActionText) : String.format(i18n._hidden('Edit {0}'), this.i18nRecordName), disabled: true, handler: this.onEditInNewWindow.createDelegate(this, ["edit"], 0), iconCls: 'action_edit' }); this.action_addInNewWindow = new Ext.Action({ requiredGrant: 'addGrant', text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(i18n._hidden('Add {0}'), this.i18nRecordName), handler: this.onEditInNewWindow.createDelegate(this, ["add"], 0), iconCls: 'action_add' }); this.action_cut = new Ext.Action({ requiredGrant: 'deleteGrant', text: this.app.i18n._('Cut event'), handler: this.onCutEvent.createDelegate(this), iconCls: 'action_cut' }); this.action_copy_to = new Ext.Action({ requiredGrant: 'deleteGrant', text: this.app.i18n._('Copy the event to the clipboard'), handler: this.onCopyToEvent.createDelegate(this), iconCls: 'action_copy' }); this.action_cancelPasting = new Ext.Action({ requiredGrant: 'deleteGrant', text: this.app.i18n._('Stop cut / copy & paste'), handler: this.onCutCancelEvent.createDelegate(this), iconCls: 'action_cut_break' }); // note: unprecise plural form here, but this is hard to change this.action_deleteRecord = new Ext.Action({ requiredGrant: 'deleteGrant', allowMultiple: true, singularText: this.i18nDeleteActionText ? i18nDeleteActionText[0] : String.format(i18n.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordName), pluralText: this.i18nDeleteActionText ? i18nDeleteActionText[1] : String.format(i18n.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordsName), translationObject: this.i18nDeleteActionText ? this.app.i18n : i18n, text: this.i18nDeleteActionText ? this.i18nDeleteActionText[0] : String.format(i18n.n_hidden('Delete {0}', 'Delete {0}', 1), this.i18nRecordName), handler: this.onDeleteRecords, disabled: true, iconCls: 'action_delete', scope: this }); this.actions_print = new Ext.Action({ requiredGrant: 'readGrant', text: this.app.i18n._('Print Page'), handler: this.onPrint.createDelegate(this, []), iconCls:'action_print', scope: this, listeners: { arrowclick: this.onPrintMenuClick.createDelegate(this) }, menu:{ items:[{ text: this.app.i18n._('Loose-Leaf'), iconCls: 'cal-print-loose-leaf', handler: this.onPrint.createDelegate(this, ['grid']) }, { text: this.app.i18n._('Sheet'), iconCls: 'cal-week-view', handler: this.onPrint.createDelegate(this, ['sheet']), disabled: Ext.isNewIE }] } }); this.showSheetView = new Ext.Button({ pressed: this.isActiveView('Sheet'), scale: 'medium', minWidth: 60, rowspan: 2, iconAlign: 'top', requiredGrant: 'readGrant', text: this.app.i18n._('Sheet'), handler: this.changeView.createDelegate(this, ["sheet"]), iconCls:'cal-sheet-view-type', xtype: 'tbbtnlockedtoggle', toggleGroup: 'Calendar_Toolbar_tgViewTypes', scope: this }); this.showGridView = new Ext.Button({ pressed: this.isActiveView('Grid'), scale: 'medium', minWidth: 60, rowspan: 2, iconAlign: 'top', requiredGrant: 'readGrant', text: this.app.i18n._('Grid'), handler: this.changeView.createDelegate(this, ["grid"]), iconCls:'cal-grid-view-type', xtype: 'tbbtnlockedtoggle', toggleGroup: 'Calendar_Toolbar_tgViewTypes', scope: this }); this.showTimelineView = new Ext.Button({ pressed: this.isActiveView('Timeline'), scale: 'medium', minWidth: 60, rowspan: 2, iconAlign: 'top', requiredGrant: 'readGrant', text: this.app.i18n._('Timeline'), handler: this.changeView.createDelegate(this, ["timeline"]), iconCls:'cal-timeline-view-type', xtype: 'tbbtnlockedtoggle', toggleGroup: 'Calendar_Toolbar_tgViewTypes', scope: this }); this.showDayView = new Ext.Toolbar.Button({ pressed: this.isActiveView('Day'), text: this.app.i18n._('Day'), iconCls: 'cal-day-view', xtype: 'tbbtnlockedtoggle', handler: this.changeView.createDelegate(this, ["day"]), enableToggle: true, toggleGroup: 'Calendar_Toolbar_tgViews' }); this.showWeekView = new Tine.Calendar.PagingToolbar.WeekPeriodPicker.Button({ pressed: this.isActiveView('Week'), text: this.app.i18n._('Week'), iconCls: 'cal-week-view', xtype: 'tbbtnlockedtoggle', handler: this.changeView.createDelegate(this, ["week"]), enableToggle: true, toggleGroup: 'Calendar_Toolbar_tgViews', listeners: { change: this.recreatePanel.createDelegate(this, ['weekSheet']) } }); this.showMonthView = new Ext.Toolbar.Button({ pressed: this.isActiveView('Month'), text: this.app.i18n._('Month'), iconCls: 'cal-month-view', xtype: 'tbbtnlockedtoggle', handler: this.changeView.createDelegate(this, ["month"]), enableToggle: true, toggleGroup: 'Calendar_Toolbar_tgViews' }); this.showYearView = new Ext.Toolbar.Button({ pressed: String(this.activeView).match(/^year/i), hidden: !this.activeView.match(/grid/i) || !this.app.featureEnabled('featureYearView'), text: this.app.i18n._('Year'), iconCls: 'cal-year-view', xtype: 'tbbtnlockedtoggle', handler: this.changeView.createDelegate(this, ["year"]), enableToggle: true, toggleGroup: 'Calendar_Toolbar_tgViews', checkState: (mainScreen, btn) => { _.defer(() => {btn.setVisible(this.activeView.match(/grid/i) || this.app.featureEnabled('featureYearView'));}); } }); this.showCustomView = new Ext.Toolbar.Button({ pressed: String(this.activeView).match(/^custom/i), tooltip: this.app.i18n._('Custom Selection'), text: ' ', iconCls: 'cal-customperiod-view', xtype: 'tbbtnlockedtoggle', handler: this.changeView.createDelegate(this, ["custom"]), enableToggle: true, toggleGroup: 'Calendar_Toolbar_tgViews', hidden: !this.activeView.match(/grid/i), checkState: (mainScreen, btn) => { let isGridView = mainScreen.activeView.match(/grid/i); btn.setVisible(isGridView); } }); this.toggleFullScreen = new Ext.Toolbar.Button({ text: '\u2197', scope: this, handler: function() { if (this.ownerCt.ref == 'tineViewportMaincardpanel') { Tine.Tinebase.viewport.tineViewportMaincardpanel.remove(this, false); Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem); this.originalOwner.add(this); this.originalOwner.layout.setActiveItem(this); this.toggleFullScreen.setText('\u2197'); this.southPanel.expand(); } else { this.originalOwner = this.ownerCt; this.originalOwner.remove(this, false); Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem = Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.activeItem; Tine.Tinebase.viewport.tineViewportMaincardpanel.add(this); Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(this); this.toggleFullScreen.setText('\u2199'); this.southPanel.collapse(); } } }); this.action_import = new Ext.Action({ requiredGrant: 'addGrant', text: this.app.i18n._('Import Events'), disabled: false, handler: this.onImport, minWidth: 60, iconCls: 'action_import', scope: this, allowMultiple: true }); this.action_export = Tine.widgets.exportAction.getExportButton( Tine.Calendar.Model.Event, { getExportOptions: (function() { return { filter: this.getAllFilterData({ noPeriodFilter: false }) } }).createDelegate(this) }, Tine.widgets.exportAction.SCOPE_MULTI); // FIXME: how can this be null? is it ok?? if (this.action_export) { this.action_export.setDisabled(false); this.action_export.setText(this.action_export.initialConfig.pluralText); } this.changeViewActions = [ this.showDayView, this.showWeekView, this.showMonthView, this.showYearView, this.showCustomView ]; this.changeViewActions.push(this.toggleFullScreen); this.recordActions = [ this.action_editInNewWindow, this.action_deleteRecord ]; this.actionUpdater = new Tine.widgets.ActionUpdater({ actions: this.recordActions, recordClass: this.recordClass, }); }, /** * get current view type (Grid/Day/Week/...) * * @param {String} view * @returns {Boolean} */ isActiveView: function(view) { var re = new RegExp(String(view).toLowerCase(), 'i'); return String(this.activeView).match(re); }, /** * returns the paste action * * @param {Date} datetime * @param {Tine.Calendar.Model.Event} event */ getPasteAction: function(datetime, event) { var shortSummary = Ext.util.Format.ellipsis(event.get('summary'), 15); return new Ext.Action({ requiredGrant: 'addGrant', text: String.format(this.app.i18n._('Paste event "{0}"'), shortSummary), handler: this.onPasteEvent.createDelegate(this, [datetime]), iconCls: 'action_paste' }); }, getActionToolbar: Tine.widgets.grid.GridPanel.prototype.getActionToolbar, onActionToolbarResize: Tine.widgets.grid.GridPanel.prototype.onActionToolbarResize, getActionToolbarItems: function() { var items = [this.action_import]; if (this.action_export) { items = items.concat(this.action_export); } return [{ xtype: 'buttongroup', columns: 1, rows: 2, frame: false, items: items }, {xtype: 'tbseparator'}, { xtype: 'buttongroup', frame: false, plugins: [{ ptype: 'ux.itemregistry', key: 'Calendar-MainScreenPanel-ViewBtnGrp' }], items: [ this.showSheetView, this.showTimelineView, this.showGridView ] }]; }, /** * @private * * NOTE: Order of items matters! Ext.Layout.Border.SplitRegion.layout() does not * fence the rendering correctly, as such it's impotant, so have the ftb * defined after all other layout items */ initLayout: function () { this.items = [{ region: 'center', layout: 'card', activeItem: 0, border: false, items: [this.getCalendarPanel(this.activeView)] }]; // add detail panel if (this.detailsPanel) { this.items.push({ region: 'south', ref: 'southPanel', border: false, collapsible: true, collapseMode: 'mini', header: false, split: true, layout: 'fit', height: this.detailsPanel.defaultHeight ? this.detailsPanel.defaultHeight : 125, items: this.detailsPanel }); } // add filter toolbar if (this.filterToolbar) { this.items.push({ region: 'north', border: false, items: this.filterToolbar, listeners: { scope: this, afterlayout: function (ct) { ct.suspendEvents(); ct.setHeight(this.filterToolbar.getHeight()); ct.ownerCt.layout.layout(); ct.resumeEvents(); } } }); } }, /** * import events * * @param {Button} btn */ onImport: function(btn) { Tine.Calendar.ImportDialog.openWindow({ appName: 'Calendar', modelName: 'Event', defaultImportContainer: this.app.getMainScreen().getWestPanel().getContainerTreePanel().getDefaultContainer('defaultContainer'), ignoreConflicts: true, doTryRun: false, // update grid after import listeners: { scope: this, 'finish': function() { this.refresh(); } } }); }, /** * @private */ onRender: function(ct, position) { Tine.Calendar.MainScreenCenterPanel.superclass.onRender.apply(this, arguments); this.loadMask = new Ext.LoadMask(this.body, {msg: this.loadMaskText, msgCls: 'x-mask-loading cal-ms-panel-mask'}); var defaultFavorite = Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(this.app.appName, this.recordClass.prototype.modelName); if (this.initialLoadAfterRender) { if (defaultFavorite) { this.selectFavorite(defaultFavorite); } else { this.refresh(); } } }, selectFavorite: function(favorite) { var favorite = favorite || Tine.widgets.persistentfilter.model.PersistentFilter.getDefaultFavorite(this.app.appName, this.recordClass.prototype.modelName), favoritesPanel = this.app.getMainScreen().getWestPanel().getFavoritesPanel(); favoritesPanel.selectFilter(favorite); }, getViewParts: function (view) { view = String(view); var activeView = String(this.activeView), periodMatch = view.match(this.periodRe), period = Ext.isArray(periodMatch) ? periodMatch[0] : null, activePeriodMatch = activeView.match(this.periodRe), activePeriod = Ext.isArray(activePeriodMatch) ? activePeriodMatch[0] : 'week', presentationMatch = view.match(this.presentationRe), presentation = Ext.isArray(presentationMatch) ? presentationMatch[0] : null, activePresentationMatch = activeView.match(this.presentationRe), activePresentation = Ext.isArray(activePresentationMatch) ? activePresentationMatch[0] : 'sheet'; return { period: period ? period : activePeriod, presentation: presentation ? presentation : activePresentation, toString: function() {return this.period + Ext.util.Format.capitalize(this.presentation);} }; }, changeView: function (view, startDate) { // autocomplete view var viewParts = this.getViewParts(view); if (viewParts.period === 'custom' && viewParts.presentation !== 'Grid') { // transition customGrid -> sheetView let range = Ext.ux.form.PeriodPicker.getRange(this.getCalendarPanel(this.activeView).getView().getPeriod()); if (['month', 'week', 'day'].indexOf(range) < 0) range = 'week'; viewParts.period = range; ; } view = viewParts.toString(); Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::changeView(' + view + ',' + startDate + ')'); // save current startDate this.startDates[this.activeView] = this.startDate.clone(); if (startDate && Ext.isDate(startDate)) { this.startDate = startDate.clone(); } else { // see if a recent startDate of that view fits var lastStartDate = this.startDates[view], currentPeriod = this.getCalendarPanel(this.activeView).getView().getPeriod(); if (Ext.isDate(lastStartDate) && lastStartDate.between(currentPeriod.from, currentPeriod.until)) { this.startDate = this.startDates[view].clone(); } } var panel = this.getCalendarPanel(view); var cardPanel = this.items.first(); if (panel.rendered) { cardPanel.layout.setActiveItem(panel.id); } else { cardPanel.add(panel); cardPanel.layout.setActiveItem(panel.id); cardPanel.doLayout(); } this.activeView = view; this.movePeriodBtns(panel.tbar); this['show' + Ext.util.Format.capitalize(viewParts.period) + 'View'].toggle(true); this['show' + Ext.util.Format.capitalize(viewParts.presentation) + 'View'].toggle(true); // update actions this.updateEventActions(); // update data // NOTE: monthView periods differ for views. // - sheetView begins with end of last month and ends with beginning of next month // - timelineView starts with the first and ends with the last // BUT startDate sticks to first of month! // monthSheet is a bitch! panel.getView().updatePeriod({from: this.startDate}, currentPeriod); panel.getStore().load({}); this.fireEvent('changeview', this, view); }, movePeriodBtns: function(tgt) { // NOTE: we need to dereference btn's in source view, oterwise btn's get destroyed on cmp destroy var rightRow = Ext.get(Ext.DomQuery.selectNode('tr[class=x-toolbar-right-row]', tgt.dom)); for (var i = this.changeViewActions.length - 1; i >= 0; i--) { if (_.isFunction(this.changeViewActions[i].checkState)) { this.changeViewActions[i].checkState(this, this.changeViewActions[i]); } rightRow.insertFirst(this.changeViewActions[i].getEl().parent().dom); } }, /** * returns all filter data for current view */ getAllFilterData: function (options) { var store = this.getCalendarPanel(this.activeView).getStore(); options = Ext.apply({ refresh: true, // ommit loadMask noPeriodFilter: true }, options || {}); this.onStoreBeforeload(store, options); return options.params.filter; }, getCustomfieldFilters: Tine.widgets.customfields.FilterModel.prototype.getCustomfieldFilters, /** * returns store of currently active view */ getStore: function () { return this.getCalendarPanel(this.activeView).getStore(); }, /** * onContextMenu * * @param {Event} e */ onContextMenu: function (e) { e.stopEvent(); var view = this.getCalendarPanel(this.activeView).getView(); var event = view.getTargetEvent(e); var datetime = view.getTargetDateTime(e); if (event && event.id.match(/new-ext-gen/) ) { // new event, no ctx menu possible atm // @see 0008948: deactivate context menu on new events return; } var addAction, responseAction, copyAction, eventStatusAction; if (datetime || event) { var dtStart = datetime || event.get('dtstart').clone(); if (dtStart.format('H:i') === '00:00') { dtStart = dtStart.add(Date.HOUR, 9); } addAction = { text: this.i18nAddActionText ? this.app.i18n._hidden(this.i18nAddActionText) : String.format(i18n._hidden('Add {0}'), this.i18nRecordName), handler: this.onEditInNewWindow.createDelegate(this, ["add", null, null, {dtStart: dtStart, is_all_day_event: datetime && datetime.is_all_day_event}]), iconCls: 'action_add' }; // assemble event actions if (event) { responseAction = this.getResponseAction(event); copyAction = this.getCopyAction(event); eventStatusAction = this.getEventStatusAction(event); } } else { addAction = this.action_addInNewWindow; } if (event) { view.getSelectionModel().select(event, e, e.ctrlKey); } else { view.getSelectionModel().clearSelections(); } var menuitems = this.recordActions.concat(addAction, copyAction || [], eventStatusAction || [], '-', responseAction || []); if (event && event.get('poll_id') && event.get('editGrant')) { require('./PollSetDefiniteEventAction'); menuitems = menuitems.concat(['-',{ text: this.app.i18n._('Set as a definite event'), iconCls: 'cal-polls-set-definite-action', scope: this, handler: function() { var me = this, ns = Tine.Calendar.eventActions.setDefiniteEventAction; ns.confirm() .then(function() { me.loadMask.show(); return Tine.Calendar.setDefinitePollEvent(event.data); }) .then(function() { me.getStore().load({refresh: true}); }) .catch(function(error) { me.loadMask.hide(); }); } }]); } if (event) { this.action_copy_to.setDisabled(event.isRecurInstance() || event.isRecurException() || event.isRecurBase()); menuitems = menuitems.concat(['-', this.action_cut, this.action_copy_to, '-']); } else if (Tine.Tinebase.data.Clipboard.has('Calendar', 'Event')) { menuitems = menuitems.concat(['-', this.getPasteAction(datetime, Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event', true)), this.action_cancelPasting, '-']); } var ctxMenu = new Ext.menu.Menu({ plugins: [{ ptype: 'ux.itemregistry', key: 'Calendar-MainScreenPanel-ContextMenu', config: { event: event, datetime: datetime } }, // to allow gridpanel hooks (like email compose) { ptype: 'ux.itemregistry', key: 'Calendar-Event-GridPanel-ContextMenu' }, { ptype: 'ux.itemregistry', key: 'Tinebase-MainContextMenu' }], items: menuitems }); // run action updater ctxMenu.showAt(e.getXY()); }, /** * get response action * * @param {Tine.Calendar.Model.Event} event * @return {Object} */ getResponseAction: function(event) { var statusStore = Tine.Tinebase.widgets.keyfield.StoreMgr.get('Calendar', 'attendeeStatus'), myAttenderRecord = event.getMyAttenderRecord(), myAttenderStatus = myAttenderRecord ? myAttenderRecord.get('status') : null, myAttenderStatusRecord = statusStore.getById(myAttenderStatus), responseAction = null; if (myAttenderRecord) { responseAction = { text: this.app.i18n._('Set my response'), icon: myAttenderStatusRecord ? myAttenderStatusRecord.get('icon') : false, menu: [] }; statusStore.each(function(status) { var isCurrent = myAttenderRecord.get('status') === status.id; // NOTE: we can't use checked items here as we use icons already responseAction.menu.push({ text: status.get('i18nValue'), handler: this.setResponseStatus.createDelegate(this, [event, status.id]), icon: status.get('icon'), disabled: myAttenderRecord.get('status') === status.id }); }, this); } return responseAction; }, /** * get copy action * * @param {Tine.Calendar.Model.Event} event * @return {Object} */ getCopyAction: function(event) { var copyAction = { text: String.format(this.app.i18n._('Copy {0}'), this.i18nRecordName), handler: this.onEditInNewWindow.createDelegate(this, ["copy", event]), iconCls: 'action_editcopy', // TODO allow to copy recurring events / exceptions disabled: event.isRecurInstance() || event.isRecurException() || event.isRecurBase() }; return copyAction }, /** * get eventStatusAction action * * @param {Tine.Calendar.Model.Event} event * @return {Object} */ getEventStatusAction: function(event) { const statusField = new Tine.Tinebase.widgets.keyfield.ComboBox({app: 'Calendar', keyFieldName: 'eventStatus'}); const statusRecord = statusField.store.getById(event.get('status')); const constraintsProvider = Tine.Calendar.EventEditDialog.getCheckStateProviders('status'); const editDialogMock = { getForm() { return { findField() { return statusField; } }} }; _.each(constraintsProvider, (cp) => { cp(editDialogMock, event); }); let eventStatusAction = { text: this.app.i18n._('Set event status'), icon: statusRecord ? statusRecord.get('icon') : false, menu: [] }; statusField.store.each(function(status) { // NOTE: we can't use checked items here as we use icons already eventStatusAction.menu.push({ text: status.get('i18nValue'), handler: this.setEventStatus.createDelegate(this, [event, status.id]), icon: status.get('icon'), disabled: status.id === event.get('status') }); }, this); return eventStatusAction; }, checkPastEvent: function(event, checkBusyConflicts, actionType, oldEvent) { var start = event.get('dtstart').getTime(); var morning = new Date().clearTime().getTime(); switch (actionType) { case 'update': var title = this.app.i18n._('Updating event in the past'), optionYes = this.app.i18n._('Update this event'), optionNo = this.app.i18n._('Do not update this event'); break; case 'add': default: var title = this.app.i18n._('Creating an event in the past'), optionYes = this.app.i18n._('Create this event'), optionNo = this.app.i18n._('Do not create this event'); } if(start < morning) { Tine.widgets.dialog.MultiOptionsDialog.openWindow({ title: title, height: 170, scope: this, options: [ {text: optionYes, name: 'yes'}, {text: optionNo, name: 'no'} ], handler: function(option) { try { switch (option) { case 'yes': if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent); else this.onAddEvent(event, checkBusyConflicts, true); break; case 'no': default: try { var panel = this.getCalendarPanel(this.activeView); if(panel) { var store = this.getStore(), view = panel.getView(); } } catch(e) { var panel = null, store = null, view = null; } if (actionType == 'add') { if(store) store.remove(event); } else { if (view && view.rendered) { this.loadMask.show(); store.reload(); // TODO: restore original event so no reload is needed // var updatedEvent = event; // updatedEvent.dirty = false; // updatedEvent.editing = false; // updatedEvent.modified = null; // // store.replaceRecord(event, updatedEvent); // view.getSelectionModel().select(event); } this.setLoading(false); } } } catch (e) { Tine.log.error('Tine.Calendar.MainScreenCenterPanel::checkPastEvent::handler'); Tine.log.error(e); } } }); } else { if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent); else this.onAddEvent(event, checkBusyConflicts, true); } }, /** * bus notified about record changes */ onRecordChanges: async function(data, e) { const panel = this.getCalendarPanel(this.activeView), store = this.getStore(), existingRecord = store.getById(data.id), isSelected = existingRecord ? panel.getSelectionModel().isSelected(existingRecord) : false; if (existingRecord && e.topic.match(/\.update/)) { const updatedRecord = Tine.Tinebase.data.Record.setFromJson(data, Tine.Calendar.Model.Event); store.remove(existingRecord); store.add([updatedRecord]); if (isSelected) { panel.getSelectionModel().select(updatedRecord); } } else if (existingRecord && e.topic.match(/\.delete/)) { store.remove(existingRecord); } else { // we can't evaluate the filters on client side to check compute if this affects us // so just lets reload this.refresh({refresh: true}); } this.updateEventActions(); }, onAddEvent: function(event, checkBusyConflicts, pastChecked) { if(!pastChecked) { this.checkPastEvent(event, checkBusyConflicts, 'add'); return; } this.setLoading(true); // remove temporary id if (event.get('id').match(/new/)) { event.set('id', ''); } if (event.isRecurBase()) { if (this.loadMask) { this.loadMask.show(); } } var panel = this.getCalendarPanel(this.activeView), store = this.getStore(); Tine.Calendar.backend.saveRecord(event, { scope: this, success: function(createdEvent) { this.congruenceFilterCheck(event, createdEvent); }, failure: this.onProxyFail.createDelegate(this, [event], true) }, { checkBusyConflicts: checkBusyConflicts === false ? 0 : 1 }); }, onUpdateEvent: function(event, pastChecked, oldEvent) { this.setLoading(true); if(!pastChecked) { this.checkPastEvent(event, null, 'update', oldEvent); return; } if (event.isRecurBase() && this.loadMask) { this.loadMask.show(); } if (event.id && (event.isRecurInstance() || event.isRecurException() || (event.isRecurBase() && ! event.get('rrule').newrule))) { var options = []; options.push({text: this.app.i18n._('Update this event only'), name: 'this'}); // if options.push({text: this.app.i18n._('Update this and all future events'), name: (event.isRecurBase() && ! event.get('rrule').newrule) ? 'series' : 'future'}); // endif options.push({text: this.app.i18n._('Update nothing'), name: 'cancel'}); Tine.widgets.dialog.MultiOptionsDialog.openWindow({ title: this.app.i18n._('Update Event'), height: 170, scope: this, options: options, handler: function(option) { var store = this.getStore(); switch (option) { case 'series': if (this.loadMask) { this.loadMask.show(); } var options = { scope: this, success: function(updatedEvent) { this.congruenceFilterCheck(event, updatedEvent); } }; options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.updateRecurSeries.createDelegate(Tine.Calendar.backend, [event, false, options])], true); if (event.isRecurException()) { Tine.Calendar.backend.saveRecord(event, options, {range: 'ALL', checkBusyConflicts: true}); } else { Tine.Calendar.backend.updateRecurSeries(event, true, options); } break; case 'this': case 'future': var options = { scope: this, success: function(updatedEvent) { this.congruenceFilterCheck(event, updatedEvent); }, failure: this.onProxyFail.createDelegate(this, [event], true) }; if (event.isRecurException()) { var range = (option === 'this' ? 'THIS' : 'THISANDFUTURE'); options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.saveRecord.createDelegate(Tine.Calendar.backend, [event, options, {range: range, checkBusyConflicts: false}])], true); Tine.Calendar.backend.saveRecord(event, options, {range: range, checkBusyConflicts: true}); } else { options.failure = this.onProxyFail.createDelegate(this, [event, Tine.Calendar.backend.createRecurException.createDelegate(Tine.Calendar.backend, [event, false, option == 'future', false, options])], true); Tine.Calendar.backend.createRecurException(event, false, option === 'future', true, options); } break; default: if(this.loadMask) { //no loadMask called from another app this.loadMask.show(); store.load({refresh: true}); } break; } } }); } else { this.onUpdateEventAction(event); } }, onUpdateEventAction: function(event) { var panel = this.getCalendarPanel(this.activeView), store = this.getStore(), view = panel.getView(); Tine.Calendar.backend.saveRecord(event, { scope: this, success: function(updatedEvent) { this.congruenceFilterCheck(event, updatedEvent); }, failure: this.onProxyFail.createDelegate(this, [event], true) }, { checkBusyConflicts: 1 }); }, /** * checks, if the last filter still matches after update * * @param {Tine.Calendar.Model.Event} event * @param {Tine.Calendar.Model.Event} updatedEvent */ congruenceFilterCheck: function(event, updatedEvent) { var filterData = this.getAllFilterData(), panel = this.getCalendarPanel(this.activeView), store = this.getStore(), view = panel.getView(), isSelected = panel.getSelectionModel().isSelected(event), me = this, promise = Promise.resolve(); if (updatedEvent.ui) { updatedEvent.ui.markDirty(); } store.replaceRecord(event, updatedEvent); if (isSelected) { panel.getSelectionModel().select(updatedEvent); } if (! event.inPeriod(view.getPeriod())) { view.updatePeriod({from: event.get('dtstart')}); promise = store.promiseLoad({}); } else if (event.isRecurBase() || updatedEvent.isRecurBase() || updatedEvent.isRecurException() // NOTE: we also need to refresh for 'this' as otherwise baseevent is not refreshed -> concurrency failures || event.hasPoll() || updatedEvent.hasPoll()) { promise = store.promiseLoad({refresh: true}); } promise.then(function() { me.setLoading(true); filterData[0].filters[0].filters.push({field: 'id', operator: 'in', value: [ updatedEvent.get('id') ]}); filterData.push({field: 'period', operator: 'within', value: me.getCalendarPanel(me.activeView).getView().getPeriod()}); Tine.Calendar.searchEvents(filterData, {}, /* fixed calendars */ true, function(r) { if (updatedEvent.ui) { updatedEvent.ui.clearDirty(); } if(r.totalcount == 0) { var renderedEvent = me.getStore().getById(updatedEvent.id); if (! renderedEvent) { me.getStore().add(updatedEvent); renderedEvent = updatedEvent; } if (renderedEvent.ui) { renderedEvent.ui.markOutOfFilter(); renderedEvent.ui.clearDirty(); } } me.setLoading(false); }, me); }); }, onDeleteRecords: function () { const panel = this.getCalendarPanel(this.activeView); const selection = panel.getSelectionModel().getSelectedEvents(); let containsRecurBase = false, containsRecurInstance = false, containsRecurException = false; Ext.each(selection, function (event) { if(event.ui) event.ui.markDirty(); if (event.isRecurInstance()) { containsRecurInstance = true; } if (event.isRecurBase()) { containsRecurBase = true; } if (event.isRecurException()) { containsRecurException = true; } }); if (selection.length > 1 && (containsRecurBase || containsRecurInstance)) { Ext.Msg.show({ title: this.app.i18n._('Please Change Selection'), msg: this.app.i18n._('Your selection contains recurring events. Recurring events must be deleted separately!'), icon: Ext.MessageBox.INFO, buttons: Ext.Msg.OK, scope: this, fn: function () { this.onDeleteRecordsConfirmFail(panel, selection); } }); return; } if (selection.length === 1 && (containsRecurBase || containsRecurInstance || containsRecurException)) { this.deleteMethodWin = Tine.widgets.dialog.MultiOptionsDialog.openWindow({ title: this.app.i18n._('Delete Event'), scope: this, height: 170, options: [ {text: this.app.i18n._('Delete only this event'), name: 'this'}, {text: this.app.i18n._('Delete this and all future events'), name: containsRecurBase ? 'all' : 'future'}, {text: this.app.i18n._('Delete the whole series'), name: 'all'}, {text: this.app.i18n._('Delete nothing'), name: 'nothing'} ], handler: function (option) { try { switch (option) { case 'all': case 'this': case 'future': panel.getTopToolbar().beforeLoad(); if (option !== 'this') { this.loadMask.show(); } var options = { scope: this, success: function () { if (option === 'this') { Ext.each(selection, function (event) { panel.getStore().remove(event); }); } this.refresh(true); } }; if (containsRecurException) { var range = (option === 'future') ? 'THISANDFUTURE' : Ext.util.Format.uppercase(option); Tine.Calendar.backend.deleteRecords([selection[0]], options, {range: range}); } else { if (option === 'all') { Tine.Calendar.backend.deleteRecurSeries(selection[0], options); } else { Tine.Calendar.backend.createRecurException(selection[0], true, option === 'future', false, options); } } break; default: this.onDeleteRecordsConfirmFail(panel, selection); break; } } catch (e) { Tine.log.error('Tine.Calendar.MainScreenCenterPanel::onDeleteRecords::handle'); Tine.log.error(e); } } }); return; } // else var i18nQuestion = String.format(this.app.i18n.ngettext('Do you really want to delete this event?', 'Do you really want to delete the {0} selected events?', selection.length), selection.length); Ext.MessageBox.confirm(i18n._hidden('Confirm'), i18nQuestion, function (btn) { if (btn === 'yes') { this.onDeleteRecordsConfirmNonRecur(panel, selection); } else { this.onDeleteRecordsConfirmFail(panel, selection); } }, this); }, onDeleteRecordsConfirmNonRecur: function (panel, selection) { panel.getTopToolbar().beforeLoad(); // create a copy of selection so selection changes don't affect this var sel = Ext.unique(selection); var options = { scope: this, success: function () { panel.getTopToolbar().onLoad(); Ext.each(sel, function (event) { panel.getStore().remove(event); }); }, failure: function () { panel.getTopToolbar().onLoad(); Ext.MessageBox.alert(i18n._hidden('Failed'), String.format(this.app.i18n.n_('Failed to delete event', 'Failed to delete the {0} events', selection.length), selection.length)); } }; Tine.Calendar.backend.deleteRecords(selection, options); }, onDeleteRecordsConfirmFail: function (panel, selection) { Ext.each(selection, function (event) { event.ui.clearDirty(); }); }, /** * is called on action cut * * @param {} action * @param {} event */ onCutEvent: function(action, event) { var panel = this.getCalendarPanel(this.activeView); var selection = panel.getSelectionModel().getSelectedEvents(); if (Ext.isArray(selection) && selection.length === 1) { event = selection[0]; } if (event.ui) { event.ui.markDirty(); } event.view = event.view ? event.view : panel.view; Tine.Tinebase.data.Clipboard.push(event); }, /** * Is called on copy to clipboard * * @param action * @param event */ onCopyToEvent: function(action, event) { var panel = this.getCalendarPanel(this.activeView); var selection = panel.getSelectionModel().getSelectedEvents(); if (Ext.isArray(selection) && selection.length === 1) { event = selection[0]; } event.isCopy = true; event.view = event.view ? event.view : panel.view; Tine.Tinebase.data.Clipboard.push(event); }, /** * is called on cancelling cut & paste */ onCutCancelEvent: function() { var store = this.getStore(); var ids = Tine.Tinebase.data.Clipboard.getIds('Calendar', 'Event'); for (var index = 0; index < ids.length; index++) { var record = store.getAt(store.findExact('id', ids[index])); if (record && record.ui) { record.ui.clearDirty(); } } Tine.Tinebase.data.Clipboard.clear('Calendar', 'Event'); }, /** * is called on action paste * * @param {Date} datetime */ onPasteEvent: function(datetime) { var record = Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event'), isCopy = record.isCopy, sourceView = record.view, sourceRecord = record, sourceViewAttendee = sourceView.ownerCt.attendee, destinationView = this.getCalendarPanel(this.activeView).getView(), destinationViewAttendee = destinationView.ownerCt.attendee; if (! record) { return; } var dtend = record.get('dtend'), dtstart = record.get('dtstart'), eventLength = dtend - dtstart, store = this.getStore(); record.beginEdit(); if (isCopy !== true) { // remove from ui before update var oldRecord = store.getAt(store.findExact('id', record.getId())); if (oldRecord && oldRecord.hasOwnProperty('ui')) { oldRecord.ui.remove(); } } else { this.omitCopyTitle = record.hasPoll(); record = Tine.Calendar.EventEditDialog.prototype.doCopyRecordToReturn(record); record.set('editGrant', true); record.set('id', ''); record.view = sourceView; if (record.get('status') == 'CANCELED') { record.set('status', 'CONFIRMED') } // remove attender ids Ext.each(record.data.attendee, function(attender) { delete attender.id; }, this); Ext.each(record.data.event_types, function(type) { delete type.id; }, this); } // @TODO move to common function with daysView::notifyDrop parts // change attendee in split view if (sourceViewAttendee || destinationViewAttendee) { var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(sourceRecord.get('attendee')), sourceAttendee = sourceViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, sourceViewAttendee) : false, destinationAttendee = destinationViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, destinationViewAttendee) : false; if (destinationViewAttendee && !destinationAttendee) { destinationAttendee = new Tine.Calendar.Model.Attender(destinationViewAttendee.data); attendeeStore.remove(sourceAttendee); attendeeStore.add(destinationAttendee); record.view = destinationView; Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, record); } } if (datetime.is_all_day_event) { record.set('dtstart', datetime); record.set('dtend', datetime.clone().add(Date.DAY, 1).add(Date.SECOND, -1)); record.set('is_all_day_event', true); } else if (datetime.date_only) { var adoptedDtStart = datetime.clone(); adoptedDtStart.setHours(dtstart.getHours()); adoptedDtStart.setMinutes(dtstart.getMinutes()); adoptedDtStart.setSeconds(dtstart.getSeconds()); record.set('dtstart', adoptedDtStart); record.set('dtend', new Date(adoptedDtStart.getTime() + eventLength)); } else { record.set('dtstart', datetime); record.set('dtend', new Date(datetime.getTime() + eventLength)); } record.endEdit(); if (isCopy === true) { record.isCopy = true; Tine.Tinebase.data.Clipboard.push(record); if (record.ui) { record.ui.clearDirty(); } this.onAddEvent(record); } else { this.onUpdateEvent(record); } }, /** * open event in new window * * @param {String} action add|edit|copy * @param {String} event edit given event instead of selected event * @param {Object} plugins event dlg plugins * @param {Object} default properties for new items */ onEditInNewWindow: function (action, event, plugins, defaults) { if (! (event && Ext.isFunction(event.beginEdit))) { event = null; } // needed for addToEventPanel if (Ext.isObject(action)) { action = action.actionType; } if (action === 'edit' && ! event) { var panel = this.getCalendarPanel(this.activeView); var selection = panel.getSelectionModel().getSelectedEvents(); if (Ext.isArray(selection) && selection.length === 1) { event = selection[0]; } } if (action === 'edit' && ! event) { return; } if (! event) { event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), defaults), 0); event.phantom = true; if (defaults && Ext.isDate(defaults.dtStart)) { event.set('dtstart', defaults.dtStart); event.set('dtend', defaults.dtStart.add(Date.MINUTE, Tine.Calendar.Model.Event.getMeta('defaultEventDuration'))); } } Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::onEditInNewWindow() - Opening event edit dialog with action: ' + action); Tine.Calendar.EventEditDialog.openWindow({ plugins: Ext.isArray(plugins) ? Ext.encode(plugins) : null, record: Ext.encode(event.getData()), recordId: event.data.id, copyRecord: (action == 'copy'), listeners: { scope: this, update: function (eventJson) { var updatedEvent = Tine.Calendar.backend.recordReader({responseText: eventJson}); updatedEvent.dirty = true; updatedEvent.modified = {}; event.phantom = (action === 'edit'); var panel = this.getCalendarPanel(this.activeView); var store = panel.getStore(); event = store.getById(event.id); if (event && action !== 'copy') { store.replaceRecord(event, updatedEvent); } else { store.add(updatedEvent); } this.onUpdateEvent(updatedEvent, false, event); } } }); }, onKeyDown: function (e) { // no keys for quickadds etc. if (e.getTarget('input') || e.getTarget('textarea')) return; switch (e.getKey()) { case e.A: // select only current page //this.grid.getSelectionModel().selectAll(true); e.preventDefault(); break; case e.E: if (!this.action_editInNewWindow.isDisabled()) { this.onEditInNewWindow('edit'); } e.preventDefault(); break; case e.N: if (!this.action_addInNewWindow.isDisabled()) { this.onEditInNewWindow('add'); } e.preventDefault(); break; case e.DELETE: e.stopEvent(); if (! this.action_deleteRecord.isDisabled()) { this.onDeleteRecords.call(this); } break; } }, onPrintMenuClick: function(splitBtn, e) { if (! String(this.activeView).match(/(day|week)sheet$/i)) { splitBtn.menu.hide(); } }, onPrint: function(printMode) { var printMode = printMode ? printMode : this.defaultPrintMode, panel = this.getCalendarPanel(this.activeView), view = panel ? panel.getView() : null; if (view && Ext.isFunction(view.print)) { view.print(printMode); } else { Ext.Msg.alert(this.app.i18n._('Could Not Print'), this.app.i18n._('Sorry, your current view does not support printing.')); } }, /** * called before store queries for data */ onStoreBeforeload: function (store, options) { options.params = options.params || {}; // allways start with an empty filter set! // this is important for paging and sort header! options.params.filter = []; if (! options.refresh && this.rendered) { // defer to have the loadMask centered in case of rendering actions this.loadMask.show.defer(50, this.loadMask); } // note, we can't use the 'normal' plugin approach here, cause we have to deal with n stores //var calendarSelectionPlugin = this.app.getMainScreen().getWestPanel().getContainerTreePanel().getFilterPlugin(); //calendarSelectionPlugin.onBeforeLoad.call(calendarSelectionPlugin, store, options); this.filterToolbar.onBeforeLoad.call(this.filterToolbar, store, options); // add period filter as last filter to not conflict with first OR filter if (! options.noPeriodFilter) { options.params.filter.push({field: 'period', operator: 'within', value: this.getCalendarPanel(this.activeView).getView().getPeriod() }); } }, /** * called when store loaded data */ onStoreLoad: function (store, options) { if (this.rendered && _.get(options, 'keepLoadMask') !== true) { this.loadMask.hide(); } // reset autoRefresh if (window.isMainWindow && this.autoRefreshInterval) { this.autoRefreshTask.delay(this.autoRefreshInterval * 1000); } // check if store is current store if (store !== this.getCalendarPanel(this.activeView).getStore()) { Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::onStoreLoad view is not active anymore'); return; } if (this.rendered) { // update filter panel if(this.filterToolbar) { this.filterToolbar.setValue(store.proxy.jsonReader.jsonData.filter); } // update container tree Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getContainerTreePanel().getFilterPlugin().setValue(store.proxy.jsonReader.jsonData.filter); // update attendee filter grid Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getAttendeeFilter().setFilterValue(store.proxy.jsonReader.jsonData.filter); //todo: implement printing for timeline views? this.actions_print.setDisabled(this.activeView.toLowerCase().includes('timeline')); } }, /** * on store load exception * * @param {Tine.Tinebase.data.RecordProxy} proxy * @param {String} type * @param {Object} error * @param {Object} options */ onStoreLoadException: function(proxy, type, error, options) { // reset autoRefresh if (window.isMainWindow && this.autoRefreshInterval) { this.autoRefreshTask.delay(this.autoRefreshInterval * 5000); } this.setLoading(false); this.loadMask.hide(); if (! options.autoRefresh) { this.onProxyFail(error); } }, onConflict: function (error, event, ignoreFn) { var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(error.event.attendee), fbInfo = new Tine.Calendar.FreeBusyInfo(error.freebusyinfo), denyIgnore = fbInfo.getStateOfAllAttendees() == Tine.Calendar.FreeBusyInfo.states.BUSY_UNAVAILABLE; this.conflictConfirmWin = Tine.widgets.dialog.MultiOptionsDialog.openWindow({ modal: true, allowCancel: false, width: 550, height: 180 + fbInfo.attendeeCount * 14 + 12 * error.freebusyinfo.length, title: this.app.i18n._('Scheduling Conflict'), questionText: '
' + this.app.i18n._('The following attendees are busy at the requested time:') + '
' + fbInfo.getInfoByAttendee(attendeeStore, event), options: [ {text: this.app.i18n._('Ignore Conflict'), name: 'ignore', disabled: denyIgnore}, {text: this.app.i18n._('Edit Event'), name: 'edit', checked: true}, {text: this.app.i18n._('Cancel this action'), name: 'cancel'} ], scope: this, handler: function (option) { var panel = this.getCalendarPanel(this.activeView), store = this.getStore(); switch (option) { case 'ignore': if (Ext.isFunction(ignoreFn)) { ignoreFn(); } else { this.onAddEvent(event, false, true); } this.conflictConfirmWin.close(); break; case 'edit': var presentationMatch = this.activeView.match(this.presentationRe), presentation = Ext.isArray(presentationMatch) ? presentationMatch[0] : null; if (presentation != 'Grid') { var view = panel.getView(); view.getSelectionModel().select(event); // mark event as not dirty to allow edit dlg event.dirty = false; view.fireEvent('dblclick', view, event); } else { // add or edit? this.onEditInNewWindow(null, event); } this.conflictConfirmWin.close(); break; case 'cancel': default: this.conflictConfirmWin.close(); this.loadMask.show(); store.load({refresh: true}); break; } } }); }, onProxyFail: function(error, event, ignoreFn) { this.setLoading(false); if(this.loadMask) this.loadMask.hide(); if (error.code === 901) { this.onConflict(error, event, ignoreFn); } else { error.clientProps = ['component', 'proxyEvent']; error.component = this; error.proxyEvent = event; Tine.Tinebase.ExceptionHandler.handleRequestException(error); } }, refresh: function (options) { // convert old boolean argument options = Ext.isObject(options) ? options : { refresh: !!options }; Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::refresh(' + options.refresh + ')'); // reset autoRefresh it might get lost if request fails if (window.isMainWindow && this.autoRefreshInterval) { this.autoRefreshTask.delay(this.autoRefreshInterval * 2000); } var panel = this.getCalendarPanel(this.activeView); panel.getStore().load(options); // clear favorites Tine.Tinebase.appMgr.get('Calendar').getMainScreen().getWestPanel().getFavoritesPanel().getSelectionModel().clearSelections(); }, setLoading: function(bool) { if (this.rendered) { var panel = this.getCalendarPanel(this.activeView), tbar = panel.getTopToolbar(); if (tbar && tbar.loading) { tbar.loading[bool ? 'disable' : 'enable'](); } } }, setResponseStatus: function(event, status) { var myAttenderRecord = event.getMyAttenderRecord(); if (myAttenderRecord) { myAttenderRecord.set('status', status); this.updateEvent(event); } }, setEventStatus: function(event, status) { Ext.MessageBox.confirm( this.app.i18n._('Update Status for all Attendee?'), this.app.i18n._('You are about to change the status of the event itself, not just your own. Do you really want to change the event status for all attendees?'), function (btn) { if (btn === 'yes') { event.set('status', status); this.updateEvent(event); } }, this); }, updateEvent: function(event) { event.dirty = true; event.modified = {}; var panel = this.getCalendarPanel(this.activeView); var store = this.getStore(); store.replaceRecord(event, event); this.onUpdateEvent(event); }, updateEventActions: function () { var panel = this.getCalendarPanel(this.activeView); var selection = panel.getSelectionModel().getSelectedEvents(); this.actionUpdater.updateActions(selection); if (this.detailsPanel) { this.detailsPanel.onDetailsUpdate(panel.getSelectionModel()); } }, updateView: function (which) { Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::updateView(' + which + ')'); var panel = this.getCalendarPanel(which); var period = panel.getTopToolbar().getPeriod(); panel.getView().updatePeriod(period); panel.getStore().load({}); //this.updateMiniCal(); }, /** * returns requested CalendarPanel * * @param {String} which * @return {Tine.Calendar.CalendarPanel} */ getCalendarPanel: function (which) { var whichParts = this.getViewParts(which); which = whichParts.toString(); let currentCalendarPanel = _.get(this, `calendarPanels.${this.activeView}`); let currentPeriod = _.isFunction(_.get(currentCalendarPanel, 'getView')) && _.isFunction(_.get(currentCalendarPanel.getView(), 'getPeriod')) ? currentCalendarPanel.getView().getPeriod() : null; if (! this.calendarPanels[which]) { Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::getCalendarPanel creating new calender panel for view ' + which); var store = new Ext.data.JsonStore({ //autoLoad: true, id: 'id', fields: Tine.Calendar.Model.Event, proxy: Tine.Calendar.backend, reader: new Ext.data.JsonReader({}), //Tine.Calendar.backend.getReader(), listeners: { scope: this, 'beforeload': this.onStoreBeforeload, 'load': this.onStoreLoad, 'loadexception': this.onStoreLoadException }, replaceRecord: function(o, n) { var r = this.getById(o.id); // refetch record as it might be outdated in the meantime var idx = this.indexOf(r); this.remove(r); this.insert(idx, n); } }); var tbar = new Tine.Calendar.PagingToolbar({ view: whichParts.period, store: store, dtStart: this.startDate, period: _.cloneDeep(currentPeriod), periodBtn: _.get(this, 'show' + _.upperFirst(whichParts.period) + 'View'), listeners: { scope: this, // NOTE: only render the button once for the toolbars // the buttons will be moved on chageView later render: function (tbar) { for (var i = 0; i < this.changeViewActions.length; i += 1) { if (! this.changeViewActions[i].rendered) { tbar.addButton(this.changeViewActions[i]); } } } } }); tbar.on('change', this.updateView.createDelegate(this, [which]), this, {buffer: 200}); tbar.on('refresh', this.refresh.createDelegate(this, [true]), this, {buffer: 200}); if (whichParts.presentation.match(/sheet/i)) { var view; switch (which) { case 'daySheet': view = new Tine.Calendar.DaysView({ store: store, startDate: tbar.getPeriod().from, numOfDays: 1 }); break; case 'monthSheet': view = new Tine.Calendar.MonthView({ store: store, period: tbar.getPeriod() }); break; case 'yearSheet': view = new Tine.Calendar.YearView({ store: store, period: tbar.getPeriod() }); break; default: case 'weekSheet': const from = tbar.getPeriod().from; const numOfDays = Math.round((tbar.getPeriod().until.getTime() - from.getTime()) / (3600*24000)); view = new Tine.Calendar.DaysView({ store: store, startDate: from, numOfDays: numOfDays }); break; } view.on('changeView', this.changeView, this); view.on('changePeriod', function (period) { this.startDate = period.from; this.startDates[which] = this.startDate.clone(); this.updateMiniCal(); }, this); // quick add/update actions view.on('addEvent', this.onAddEvent, this); view.on('updateEvent', this.onUpdateEvent, this); this.calendarPanels[which] = new Tine.Calendar.CalendarPanel({ tbar: tbar, view: view, mainScreen: this, canonicalName: ['Event', 'Sheet'] }); } else if (whichParts.presentation.match(/grid/i)) { this.calendarPanels[which] = new Tine.Calendar.GridView({ tbar: tbar, store: store, mainScreen: this, canonicalName: ['Event', 'Grid'] }); } else if (whichParts.presentation.match(/timeline/i)) { this.calendarPanels[which] = new Tine.Calendar.TimelinePanel({ tbar: tbar, period: tbar.periodPicker.getPeriod(), viewType: whichParts.period, store: store, canonicalName: ['Event', 'Timeline'], mainScreen: this }); } this.calendarPanels[which]['canonicalName'].push(Ext.util.Format.capitalize(which.match(/[a-z]+/)[0])); this.calendarPanels[which]['canonicalName'] = this.calendarPanels[which]['canonicalName'].join(Tine.Tinebase.CanonicalPath.separator); this.calendarPanels[which].on('dblclick', this.onEditInNewWindow.createDelegate(this, ["edit"], 0)); this.calendarPanels[which].on('contextmenu', this.onContextMenu, this); this.calendarPanels[which].getSelectionModel().on('selectionchange', this.updateEventActions, this); this.calendarPanels[which].on('keydown', this.onKeyDown, this); this.calendarPanels[which].relayEvents(this, ['show', 'beforehide']); this.calendarPanels[which].cls = 'cal-ms-panel'; // rebind store so it's event listeners get called at last. // -> otherwise loading spinner would be active event if the view beforeload cancels the request tbar.unbind(store); tbar.bind(store); } return this.calendarPanels[which]; }, recreatePanel: function(which) { if (this.calendarPanels[which]) { let panel = this.getCalendarPanel(which); this.calendarPanels[which] = null; _.delay(() => { const cardPanel = this.items.first(); panel.tbar = null; // yes, this is a mem leak - but it's ok as recreate is a rare action panel.toolbars = [];// and the button handling is complicated cardPanel.remove(panel); }, 2000); if (this.activeView === which) { this.changeView(which, panel.getView().getPeriod().from); } } }, updateMiniCal: function () { var miniCal = Ext.getCmp('cal-mainscreen-minical'); var weekNumbers = null; var period = this.getCalendarPanel(this.activeView).getView().getPeriod(); switch (this.activeView) { case 'week' : weekNumbers = [period.from.add(Date.DAY, 1).getWeekOfYear()]; break; case 'month' : weekNumbers = []; var startWeek = period.from.add(Date.DAY, 1).getWeekOfYear(); var numWeeks = Math.round((period.until.getTime() - period.from.getTime()) / Date.msWEEK); for (var i = 0; i < numWeeks; i += 1) { weekNumbers.push(startWeek + i); } break; } miniCal.update(this.startDate, true, weekNumbers); }, onDestroy: function() { _.each(this.postalSubscriptions, (subscription) => {subscription.unsubscribe()}); return Tine.Calendar.MainScreenCenterPanel.superclass.onDestroy.call(this); }, });