From bec67d63ee67e8814444edf38991ea4bf4662765 Mon Sep 17 00:00:00 2001 From: nathangray Date: Fri, 7 Aug 2020 09:19:06 -0600 Subject: [PATCH] Etemplate: Change how nextmatch handles update & refresh push messages --- api/js/etemplate/et2_extension_nextmatch.js | 84 ++++++++++++----- api/js/etemplate/et2_extension_nextmatch.ts | 92 +++++++++++++------ api/js/jsapi/egw_app.js | 35 +------ api/js/jsapi/egw_app.ts | 45 +-------- infolog/inc/class.infolog_ui.inc.php | 1 + infolog/js/app.js | 2 +- infolog/js/app.ts | 2 +- .../inc/class.preferences_hooks.inc.php | 8 ++ timesheet/js/app.js | 2 +- timesheet/js/app.ts | 2 +- 10 files changed, 140 insertions(+), 133 deletions(-) diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js index 689ff02152..1dfa42a9c8 100644 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ b/api/js/etemplate/et2_extension_nextmatch.js @@ -439,18 +439,27 @@ var et2_nextmatch = /** @class */ (function (_super) { * Refresh given rows for specified change * * Change type parameters allows for quicker refresh then complete server side reload: - * - 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. + * - update: request modified data from given rows. May be moved. * - update-in-place: update row, but do NOT move it, or refresh if uid does not exist - * - edit: rows changed, but sorting may be affected. May require full reload. + * - edit: rows changed, but sorting may be affected. Full reload. * - delete: just delete the given rows clientside (no server interaction neccessary) * - add: put the new row in at the top, unless app says otherwise * + * What actually happens also depends on a general preference "lazy-update": + * default/lazy: + * - add always on top + * - updates on top, if sorted by last modified, otherwise update-in-place + * - update-in-place is always in place! + * + * exact: + * - add and update on top if sorted by last modified, otherwise full refresh + * - update-in-place is always in place! + * * Nextmatch checks the application callback nm_refresh_index, which has a default implementation * in egw_app.nm_refresh_index(). * * @param {string[]|string} _row_ids rows to refresh - * @param {?string} _type "update", "edit", "delete" or "add" + * @param {?string} _type "update-in-place", "update", "edit", "delete" or "add" * * @see jsapi.egw_refresh() * @see egw_app.nm_refresh_index() @@ -470,8 +479,16 @@ var et2_nextmatch = /** @class */ (function (_super) { .bind({ nm: this, ids: _row_ids, type: _type })); return; } + // Make some changes in what we're doing based on preference + var update_pref = egw.preference("lazy-update"); + if (_type == et2_nextmatch.UPDATE && !this.is_sorted_by_modified()) { + _type = update_pref == "lazy" ? et2_nextmatch.UPDATE_IN_PLACE : et2_nextmatch.EDIT; + } + else if (update_pref == "exact" && _type == et2_nextmatch.ADD && !this.is_sorted_by_modified()) { + _type = et2_nextmatch.EDIT; + } if (typeof _type == 'undefined') - _type = 'edit'; + _type = et2_nextmatch.EDIT; if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; if (typeof _row_ids == "undefined" || _row_ids === null) { @@ -480,7 +497,7 @@ var et2_nextmatch = /** @class */ (function (_super) { jQuery(this).triggerHandler("refresh", [this]); return; } - if (_type == "delete") { + if (_type == et2_nextmatch.DELETE) { // Record current & next index var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); @@ -513,24 +530,27 @@ var et2_nextmatch = /** @class */ (function (_super) { id_loop: for (var i = 0; i < _row_ids.length; i++) { var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; switch (_type) { - // update-in-place = update, but "update" is too ambiguous - case "update-in-place": - case "update": + // update-in-place = update, but always only in place + case et2_nextmatch.UPDATE_IN_PLACE: this.egw().dataRefreshUID(uid); break; - case "edit": + // update [existing] row, maybe we'll put it on top + case et2_nextmatch.UPDATE: if (!this.refresh_update(uid)) { // Could not update just the row, full refresh has been requested break id_loop; } break; - case "delete": - // Handled above, more code to execute after loop + case et2_nextmatch.DELETE: + // Handled above, more code to execute after loop so don't exit early break; - case "add": - if (this.refresh_add(uid)) - break; + case et2_nextmatch.ADD: + if (update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified()) { + if (this.refresh_add(uid)) + break; + } // fall-through / full refresh, if refresh_add returns false + case et2_nextmatch.EDIT: default: // Trigger refresh this.applyFilters(); @@ -558,7 +578,7 @@ var et2_nextmatch = /** @class */ (function (_super) { // and we can't always find it by UID after due to duplication this.controller._grid.deleteRow(entry.idx); // Pretend it's a new row, let app tell us where it goes and we'll mark it as new - if (!this.refresh_add(uid, "update")) { + if (!this.refresh_add(uid, et2_nextmatch.UPDATE)) { // App did not want the row, or doesn't know where it goes but we've already removed it... // Put it back before anyone notices. New data coming from server anyway. var callback_1 = function (data) { @@ -577,15 +597,10 @@ var et2_nextmatch = /** @class */ (function (_super) { * @return boolean false: not added, true: added */ et2_nextmatch.prototype.refresh_add = function (uid, type) { - if (type === void 0) { type = "add"; } - var index = 0; - var appname = this._get_appname(); - var app_obj = this.getInstanceManager().app_obj[appname] || this.egw().window.app[appname]; - if (appname && app_obj && typeof app_obj.nm_refresh_index == "function") { - var sort = Object.values(this.controller._indexMap).map(function (e) { return ({ index: e.idx, uid: e.uid }); }); - index = this.getInstanceManager().app_obj[appname].nm_refresh_index(this, uid, sort, type); - } - // App cancelled the add + if (type === void 0) { type = et2_nextmatch.ADD; } + var index = egw.preference("lazy-update") == "lazy" ? 0 : + (this.is_sorted_by_modified() ? 0 : false); + // No add, do a full refresh if (index === false) { return false; } @@ -602,6 +617,16 @@ var et2_nextmatch = /** @class */ (function (_super) { this.egw().dataRegisterUID(uid, callback, this, this.getInstanceManager().etemplate_exec_id, this.id); return true; }; + /** + * Is this nextmatch currently sorted by "modified" date + * + * This is decided by the row_modified options passed from the server and the current sort order + */ + et2_nextmatch.prototype.is_sorted_by_modified = function () { + var _a; + var sort = ((_a = this.getValue()) === null || _a === void 0 ? void 0 : _a.sort) || {}; + return sort && sort.id && sort.id == this.settings.add_on_top_sort_field && sort.asc == false; + }; et2_nextmatch.prototype._get_appname = function () { var app = ''; var list = []; @@ -2188,6 +2213,15 @@ var et2_nextmatch = /** @class */ (function (_super) { "default": {} } }; + /** + * Update types + * @see et2_nextmatch.refresh() for more information + */ + et2_nextmatch.ADD = 'add'; + et2_nextmatch.UPDATE_IN_PLACE = 'update-in-place'; + et2_nextmatch.UPDATE = 'update'; + et2_nextmatch.EDIT = 'edit'; + et2_nextmatch.DELETE = 'delete'; et2_nextmatch.legacyOptions = ["template", "hide_header", "header_left", "header_right"]; return et2_nextmatch; }(et2_core_DOMWidget_1.et2_DOMWidget)); diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index 0ef541fb1e..25b26c692f 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -220,6 +220,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 searchletter?: string }; + /** + * Update types + * @see et2_nextmatch.refresh() for more information + */ + public static readonly ADD = 'add'; + public static readonly UPDATE_IN_PLACE = 'update-in-place'; + public static readonly UPDATE = 'update'; + public static readonly EDIT = 'edit'; + public static readonly DELETE = 'delete'; + // DOM / jQuery stuff private div: JQuery; private innerDiv: JQuery; @@ -229,7 +239,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Popup to select columns private selectPopup: any; - public static legacyOptions = ["template", "hide_header", "header_left", "header_right"]; + public static readonly legacyOptions = ["template", "hide_header", "header_left", "header_right"]; private template: any; columns: { visible: boolean, widget: et2_widget }[]; @@ -696,18 +706,27 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * Refresh given rows for specified change * * Change type parameters allows for quicker refresh then complete server side reload: - * - 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. + * - update: request modified data from given rows. May be moved. * - update-in-place: update row, but do NOT move it, or refresh if uid does not exist - * - edit: rows changed, but sorting may be affected. May require full reload. + * - edit: rows changed, but sorting may be affected. Full reload. * - delete: just delete the given rows clientside (no server interaction neccessary) * - add: put the new row in at the top, unless app says otherwise * + * What actually happens also depends on a general preference "lazy-update": + * default/lazy: + * - add always on top + * - updates on top, if sorted by last modified, otherwise update-in-place + * - update-in-place is always in place! + * + * exact: + * - add and update on top if sorted by last modified, otherwise full refresh + * - update-in-place is always in place! + * * Nextmatch checks the application callback nm_refresh_index, which has a default implementation * in egw_app.nm_refresh_index(). * * @param {string[]|string} _row_ids rows to refresh - * @param {?string} _type "update", "edit", "delete" or "add" + * @param {?string} _type "update-in-place", "update", "edit", "delete" or "add" * * @see jsapi.egw_refresh() * @see egw_app.nm_refresh_index() @@ -730,7 +749,19 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 ); return; } - if (typeof _type == 'undefined') _type = 'edit'; + + // Make some changes in what we're doing based on preference + let update_pref = egw.preference("lazy-update"); + if(_type == et2_nextmatch.UPDATE && !this.is_sorted_by_modified()) + { + _type = update_pref == "lazy" ? et2_nextmatch.UPDATE_IN_PLACE : et2_nextmatch.EDIT; + } + else if (update_pref == "exact" && _type == et2_nextmatch.ADD && !this.is_sorted_by_modified()) + { + _type = et2_nextmatch.EDIT; + } + + if (typeof _type == 'undefined') _type = et2_nextmatch.EDIT; if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; if (typeof _row_ids == "undefined" || _row_ids === null) { @@ -742,7 +773,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 return; } - if(_type == "delete") + if(_type == et2_nextmatch.DELETE) { // Record current & next index var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; @@ -787,24 +818,28 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; switch(_type) { - // update-in-place = update, but "update" is too ambiguous - case "update-in-place": - case "update": + // update-in-place = update, but always only in place + case et2_nextmatch.UPDATE_IN_PLACE: this.egw().dataRefreshUID(uid); break; - case "edit": + // update [existing] row, maybe we'll put it on top + case et2_nextmatch.UPDATE: if(!this.refresh_update(uid)) { // Could not update just the row, full refresh has been requested break id_loop; } break; - case "delete": - // Handled above, more code to execute after loop + case et2_nextmatch.DELETE: + // Handled above, more code to execute after loop so don't exit early break; - case "add": - if (this.refresh_add(uid)) break; + case et2_nextmatch.ADD: + if( update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified() ) + { + if (this.refresh_add(uid)) break; + } // fall-through / full refresh, if refresh_add returns false + case et2_nextmatch.EDIT: default: // Trigger refresh this.applyFilters(); @@ -838,7 +873,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.controller._grid.deleteRow(entry.idx); // Pretend it's a new row, let app tell us where it goes and we'll mark it as new - if(!this.refresh_add(uid, "update")) + if(!this.refresh_add(uid, et2_nextmatch.UPDATE)) { // App did not want the row, or doesn't know where it goes but we've already removed it... // Put it back before anyone notices. New data coming from server anyway. @@ -859,18 +894,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * @param uid * @return boolean false: not added, true: added */ - protected refresh_add(uid:string, type = "add") + protected refresh_add(uid:string, type = et2_nextmatch.ADD) { - let index : boolean | number = 0; - let appname = this._get_appname(); - let app_obj = this.getInstanceManager().app_obj[appname] || this.egw().window.app[appname]; - if(appname && app_obj && typeof app_obj.nm_refresh_index == "function") - { - let sort = Object.values(this.controller._indexMap).map(e => ({index:e.idx, uid:e.uid})); - index = this.getInstanceManager().app_obj[appname].nm_refresh_index(this, uid, sort, type) - } + let index : boolean | number = egw.preference("lazy-update") == "lazy" ? 0 : + (this.is_sorted_by_modified() ? 0 : false); - // App cancelled the add + // No add, do a full refresh if(index === false) { return false; @@ -891,6 +920,17 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 return true; } + /** + * Is this nextmatch currently sorted by "modified" date + * + * This is decided by the row_modified options passed from the server and the current sort order + */ + public is_sorted_by_modified() + { + let sort = this.getValue()?.sort || {}; + return sort && sort.id && sort.id == this.settings.add_on_top_sort_field && sort.asc == false; + } + private _get_appname() { let app = ''; diff --git a/api/js/jsapi/egw_app.js b/api/js/jsapi/egw_app.js index f3f1ea70e8..3355c1f867 100644 --- a/api/js/jsapi/egw_app.js +++ b/api/js/jsapi/egw_app.js @@ -53,14 +53,12 @@ var EgwApp = /** @class */ (function () { * Initialization and setup goes here, but the etemplate2 object * is not yet ready. */ - function EgwApp(appname, modified_field) { - if (modified_field === void 0) { modified_field = ""; } + function EgwApp(appname) { /** * Mailvelope "egroupware" Keyring */ this.mailvelope_keyring = undefined; this.appname = appname; - this.modification_field_name = modified_field; this.egw = egw(this.appname, window); // Initialize sidebox for non-popups. // ID set server side @@ -176,37 +174,6 @@ var EgwApp = /** @class */ (function () { EgwApp.prototype.uid = function (pushData) { return pushData.app + '::' + pushData.id; }; - /** - * Callback from nextmatch so application can have some control over - * where new rows (added via push) are added. This is called when - * the type is "add" or "update". - * - * Returning false for a new row will cause nm to do a full reload of all data. - * For an update the row will be updated in place. - * - * @param nm Nextmatch the entry is going to be added to - * @param uid - * @param current_order List of ids in order - * @param update_type add or update - * - * @return number | boolean Row index (0 at the top) or false to not insert the row - */ - EgwApp.prototype.nm_refresh_index = function (nm, uid, current_order, update_type) { - var _a; - // Do we have a modified field so we can check nm sort order? - if (this.modification_field_name) { - var value = nm.getValue(); - var sort = ((_a = value) === null || _a === void 0 ? void 0 : _a.sort) || {}; - if (sort && sort.id == this.modification_field_name && sort.asc == false) { - // Sorting by modification time, DESC. Put it at the top. - return 0; - } - // Don't actually add it in. - return false; - } - // Just put it in at the top - return 0; - }; /** * Open an entry. * diff --git a/api/js/jsapi/egw_app.ts b/api/js/jsapi/egw_app.ts index de8d519e9f..7b83c6fee5 100644 --- a/api/js/jsapi/egw_app.ts +++ b/api/js/jsapi/egw_app.ts @@ -69,11 +69,6 @@ export abstract class EgwApp */ readonly appname: string; - /** - * Name of the modification timestamp in entry data - */ - readonly modification_field_name : string; - /** * Internal reference to the most recently loaded etemplate2 widget tree * @@ -140,10 +135,9 @@ export abstract class EgwApp * Initialization and setup goes here, but the etemplate2 object * is not yet ready. */ - constructor(appname: string, modified_field:string = "") + constructor(appname: string) { this.appname = appname; - this.modification_field_name = modified_field; this.egw = egw(this.appname, window); // Initialize sidebox for non-popups. @@ -279,43 +273,6 @@ export abstract class EgwApp return pushData.app + '::' + pushData.id; } - /** - * Callback from nextmatch so application can have some control over - * where new rows (added via push) are added. This is called when - * the type is "add" or "update". - * - * Returning false for a new row will cause nm to do a full reload of all data. - * For an update the row will be updated in place. - * - * @param nm Nextmatch the entry is going to be added to - * @param uid - * @param current_order List of ids in order - * @param update_type add or update - * - * @return number | boolean Row index (0 at the top) or false to not insert the row - */ - nm_refresh_index(nm: et2_nextmatch, uid: string, current_order: string[], update_type: string) : number|boolean - { - // Do we have a modified field so we can check nm sort order? - if(this.modification_field_name) - { - let value = nm.getValue(); - let sort = value?.sort || {}; - - if(sort && sort.id == this.modification_field_name && sort.asc == false) - { - // Sorting by modification time, DESC. Put it at the top. - return 0; - } - - // Don't actually add it in. - return false; - } - - // Just put it in at the top - return 0; - } - /** * Open an entry. * diff --git a/infolog/inc/class.infolog_ui.inc.php b/infolog/inc/class.infolog_ui.inc.php index 9c419c32d0..d48f5d5799 100644 --- a/infolog/inc/class.infolog_ui.inc.php +++ b/infolog/inc/class.infolog_ui.inc.php @@ -944,6 +944,7 @@ class infolog_ui $values['nm']['disable_autorefresh'] = true; // we have push $values['nm']['options-filter'] = $this->filters; $values['nm']['get_rows'] = 'infolog.infolog_ui.get_rows'; + $values['nm']['add_on_top_sort_field'] = 'info_datemodified'; $values['nm']['options-filter2'] = (in_array($this->prefs['show_links'],array('all','no_describtion')) ? array() : array( '' => 'default', )) + array( diff --git a/infolog/js/app.js b/infolog/js/app.js index 67adbd0fd3..481f666e67 100644 --- a/infolog/js/app.js +++ b/infolog/js/app.js @@ -47,7 +47,7 @@ var InfologApp = /** @class */ (function (_super) { function InfologApp() { var _this = // call parent - _super.call(this, 'infolog', 'info_datemodified') || this; + _super.call(this, 'infolog') || this; _this._action_ids = []; _this._action_all = false; return _this; diff --git a/infolog/js/app.ts b/infolog/js/app.ts index 561f78daec..18b9b54f1a 100644 --- a/infolog/js/app.ts +++ b/infolog/js/app.ts @@ -41,7 +41,7 @@ class InfologApp extends EgwApp constructor() { // call parent - super('infolog', 'info_datemodified'); + super('infolog'); } /** diff --git a/preferences/inc/class.preferences_hooks.inc.php b/preferences/inc/class.preferences_hooks.inc.php index df696cd912..b028104ae2 100644 --- a/preferences/inc/class.preferences_hooks.inc.php +++ b/preferences/inc/class.preferences_hooks.inc.php @@ -164,6 +164,14 @@ class preferences_hooks 'admin' => False, 'forced' => 20, // hidden as not used in eTemplate2 ), + 'lazy-update' => array( + 'type' => 'select', + 'label' => 'Fast updates', + 'name' => 'lazy-update', + 'values' => array('lazy' => lang('Fast'), 'exact' => lang('Full refresh')), + 'help' => 'Choose between fast updates with changes sometimes on top, or full refresh', + 'default'=> 'lazy' + ), 'template_set' => array( 'type' => 'select', 'label' => 'Interface/Template Selection', diff --git a/timesheet/js/app.js b/timesheet/js/app.js index c21c71377d..21bd2915c5 100644 --- a/timesheet/js/app.js +++ b/timesheet/js/app.js @@ -39,7 +39,7 @@ var etemplate2_1 = require("../../api/js/etemplate/etemplate2"); var TimesheetApp = /** @class */ (function (_super) { __extends(TimesheetApp, _super); function TimesheetApp() { - return _super.call(this, 'timesheet', "ts_start") || this; + return _super.call(this, 'timesheet') || this; } /** * This function is called when the etemplate2 object is loaded diff --git a/timesheet/js/app.ts b/timesheet/js/app.ts index a76a98e10e..3ffb502410 100644 --- a/timesheet/js/app.ts +++ b/timesheet/js/app.ts @@ -31,7 +31,7 @@ class TimesheetApp extends EgwApp constructor() { - super('timesheet',"ts_start"); + super('timesheet'); } /**