From dc6a8e0977d069b791e066fd9328854d53c32fc5 Mon Sep 17 00:00:00 2001 From: nathangray Date: Tue, 14 Jul 2020 13:39:45 -0600 Subject: [PATCH] * Calendar: Push updates --- calendar/inc/class.calendar_boupdate.inc.php | 12 ++- calendar/inc/class.calendar_hooks.inc.php | 1 + calendar/inc/class.calendar_ui.inc.php | 16 ++++ calendar/inc/class.calendar_uiforms.inc.php | 5 +- calendar/js/app.js | 78 +++++++++++++++- calendar/js/app.ts | 97 +++++++++++++++++++- 6 files changed, 201 insertions(+), 8 deletions(-) diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 2d20d4871e..e4eb343a61 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -118,6 +118,7 @@ class calendar_boupdate extends calendar_bo */ function update(&$event,$ignore_conflicts=false,$touch_modified=true,$ignore_acl=false,$updateTS=true,&$messages=null, $skip_notification=false) { + $update_type = 'edit'; unset($updateTS); // ignored, as updating timestamps is required for sync! //error_log(__METHOD__."(".array2string($event).",$ignore_conflicts,$touch_modified,$ignore_acl)"); if (!is_array($messages)) $messages = $messages ? (array)$messages : array(); @@ -150,6 +151,7 @@ class calendar_boupdate extends calendar_bo $status = calendar_so::combine_status($event['owner'] == $this->user ? 'A' : 'U', 1, 'CHAIR'); $event['participants'] = array($event['owner'] => $status); } + $update_type = 'add'; } // check if user has the permission to update / create the event @@ -273,6 +275,7 @@ class calendar_boupdate extends calendar_bo { // Restored, bring back links Link::restore('calendar', $cal_id); + $update_type = 'add'; } if ($this->log_file) { @@ -291,8 +294,8 @@ class calendar_boupdate extends calendar_bo } } - // notify the link-class about the update, as other apps may be subscribt to it - Link::notify_update('calendar',$cal_id,$event); + // notify the link-class about the update, as other apps may be subscribed to it + Link::notify_update('calendar',$cal_id,$event,$update_type); return $cal_id; } @@ -1777,7 +1780,10 @@ class calendar_boupdate extends calendar_bo { $this->set_status($old_event, $userid, $status, $recur_date, true, false,$skip_notification); } - } + + // notify the link-class about the update, as other apps may be subscribed to it + Link::notify_update('calendar',$new_event['id'],$new_event,"update"); + } /** * deletes an event diff --git a/calendar/inc/class.calendar_hooks.inc.php b/calendar/inc/class.calendar_hooks.inc.php index c88799b3f4..7cb912c2ec 100644 --- a/calendar/inc/class.calendar_hooks.inc.php +++ b/calendar/inc/class.calendar_hooks.inc.php @@ -68,6 +68,7 @@ class calendar_hooks 'merge' => true, 'entry' => 'Event', 'entries' => 'Events', + 'push_data' => ['id','owner','participants','start','end'] ); } diff --git a/calendar/inc/class.calendar_ui.inc.php b/calendar/inc/class.calendar_ui.inc.php index 3c5ee71bd9..44ed9d7552 100644 --- a/calendar/inc/class.calendar_ui.inc.php +++ b/calendar/inc/class.calendar_ui.inc.php @@ -617,6 +617,22 @@ class calendar_ui $sidebox->exec('calendar.calendar_ui.sidebox_etemplate', $cont, $sel_options, $readonlys); } + /** + * Get the data for the given event IDs in a format suitable for the client. + * + * Used to get new data when Push tells us. Push doesn't have the full event data, + * just the minimum, so the client needs to ask for it. + * + * @param string[] $event_ids + */ + public function ajax_get($event_ids) + { + foreach($event_ids as $id) + { + $this->update_client($id); + } + } + /** * Send updated event information to the client via ajax * diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index 02a1e39ab2..f835221fca 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -3200,9 +3200,8 @@ class calendar_uiforms extends calendar_ui } } - // Directly update stored data. If event is still visible, it will - // be notified & update itself. - $this->update_client($eventId,$d); + // notify the link-class about the update, as other apps may be subscribed to it + Link::notify_update('calendar',$event['id'],$event,"update"); } /** diff --git a/calendar/js/app.js b/calendar/js/app.js index 296d736f22..a2bba0e071 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -119,7 +119,8 @@ var CalendarApp = /** @class */ (function (_super) { egw.preference('defaultcalendar', 'calendar') || 'day', owner: egw.user('account_id'), keywords: '', - last: undefined + last: undefined, + first: undefined }; // If you are in one of these views and select a date in the sidebox, the view // will change as needed to show the date. Other views will only change the @@ -513,6 +514,81 @@ var CalendarApp = /** @class */ (function (_super) { return undefined; } }; + /** + * Handle a push notification about entry changes from the websocket + * + * @param pushData + * @param {string} pushData.app application name + * @param {(string|number)} pushData.id id of entry to refresh or null + * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: ask server for data, add in intelligently + * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary + * @param {number} pushData.account_id User that caused the notification + */ + CalendarApp.prototype.push = function (pushData) { + // Calendar cares about calendar & infolog + if (pushData.app !== this.appname && pushData.app !== 'infolog') + return; + // pushData does not contain everything, just the minimum. See calendar_hooks::search_link(). + var event = pushData.acl || {}; + if (pushData.type === 'delete') { + return _super.prototype.push.call(this, pushData); + } + switch (pushData.app) { + case "calendar": + return this.push_calendar(pushData); + case "infolog": + return this.push_infolog(pushData); + } + }; + /** + * Handle a push about infolog + * + * @param pushData + */ + CalendarApp.prototype.push_infolog = function (pushData) { + // This isn't the most intelligent, but it refreshes + this.observer("", pushData.app, pushData.id, pushData.type, "", null); + }; + /** + * Handle a push from calendar + * + * @param pushData + */ + CalendarApp.prototype.push_calendar = function (pushData) { + // check visibility - grants is ID => permission of people we're allowed to see + var owners = []; + if (typeof this._grants === 'undefined') { + this._grants = egw.grants(this.appname); + } + // Filter what's allowed down to those we care about + var filtered = Object.keys(this._grants).filter(function (account) { return app.calendar.state.owner.indexOf(account) >= 0; }); + // Check if we're interested in displaying by owner / participant + var owner_check = et2_widget_event_1.et2_calendar_event.owner_check(event, { options: { owner: filtered } }); + if (!owner_check) { + // The owner is not in the list of what we're allowed / care about + return; + } + // Check if we're interested by date? + if (event.end <= new Date(this.state.first).valueOf() / 1000 || event.start > new Date(this.state.last).valueOf() / 1000) { + // The event is outside our current view + return; + } + // Ask for the real data + egw.json("calendar.calendar_ui.ajax_get", [[pushData.id]], function (data) { + var unknown = (typeof egw.dataGetUIDdata(data.uid) === "undefined"); + // Store it, which will call all registered listeners + egw.dataStoreUID(data.uid, data.data); + // Any existing events were updated. Run this to catch new events or events moved into view + if (unknown) { + this._update_events(this.state, [data.uid]); + } + }.bind(this)).sendRequest(true); + }; /** * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app * diff --git a/calendar/js/app.ts b/calendar/js/app.ts index 17f07230ca..fb7d455c45 100644 --- a/calendar/js/app.ts +++ b/calendar/js/app.ts @@ -80,7 +80,8 @@ class CalendarApp extends EgwApp egw.preference('defaultcalendar','calendar') || 'day', owner: egw.user('account_id'), keywords: '', - last: undefined + last: undefined, + first: undefined }; /** @@ -126,6 +127,8 @@ class CalendarApp extends EgwApp // Data for quick add dialog private quick_add: any; + // + private _grants : any; /** * Constructor * @@ -438,6 +441,98 @@ class CalendarApp extends EgwApp return undefined; } } + /** + * Handle a push notification about entry changes from the websocket + * + * @param pushData + * @param {string} pushData.app application name + * @param {(string|number)} pushData.id id of entry to refresh or null + * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: ask server for data, add in intelligently + * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary + * @param {number} pushData.account_id User that caused the notification + */ + push(pushData) + { + // Calendar cares about calendar & infolog + if(pushData.app !== this.appname && pushData.app !== 'infolog') return; + + // pushData does not contain everything, just the minimum. See calendar_hooks::search_link(). + let event = pushData.acl || {}; + + if(pushData.type === 'delete') + { + return super.push(pushData); + } + + switch (pushData.app) + { + case "calendar": + return this.push_calendar(pushData); + case "infolog": + return this.push_infolog(pushData); + } + } + + /** + * Handle a push about infolog + * + * @param pushData + */ + private push_infolog(pushData) + { + // This isn't the most intelligent, but it refreshes + this.observer("", pushData.app, pushData.id, pushData.type,"",null); + } + + /** + * Handle a push from calendar + * + * @param pushData + */ + private push_calendar(pushData) + { + // check visibility - grants is ID => permission of people we're allowed to see + let owners = []; + if(typeof this._grants === 'undefined') + { + this._grants = egw.grants(this.appname); + } + // Filter what's allowed down to those we care about + let filtered = Object.keys(this._grants).filter(account => app.calendar.state.owner.indexOf(account) >= 0); + + // Check if we're interested in displaying by owner / participant + let owner_check = et2_calendar_event.owner_check(event, {options: {owner: filtered}}); + if(!owner_check) + { + // The owner is not in the list of what we're allowed / care about + return; + } + + // Check if we're interested by date? + if(event.end <= new Date(this.state.first).valueOf() /1000 || event.start > new Date(this.state.last).valueOf()/1000) + { + // The event is outside our current view + return; + } + + // Ask for the real data + egw.json("calendar.calendar_ui.ajax_get", [[pushData.id]], function(data) { + let unknown = (typeof egw.dataGetUIDdata(data.uid) === "undefined"); + // Store it, which will call all registered listeners + egw.dataStoreUID(data.uid, data.data); + + // Any existing events were updated. Run this to catch new events or events moved into view + if(unknown) + { + this._update_events(this.state, [data.uid]); + } + }.bind(this)).sendRequest(true); + } /** * Link hander for jDots template to just reload our iframe, instead of reloading whole admin app