diff --git a/addressbook/inc/class.addressbook_ui.inc.php b/addressbook/inc/class.addressbook_ui.inc.php index 0cfd39c189..b6bb817431 100644 --- a/addressbook/inc/class.addressbook_ui.inc.php +++ b/addressbook/inc/class.addressbook_ui.inc.php @@ -2956,7 +2956,10 @@ class addressbook_ui extends addressbook_bo unset($GLOBALS['egw_info']['user']['preferences']['common']['auto_hide_sidebox']); // need to load list's app.js now, as exec calls header before other app can include it - Framework::includeJS('/'.$crm_list.'/js/app.js'); + // Framework::includeJS('/'.$crm_list.'/js/app.js'); + + // Load CRM code + Framework::includeJS('.','CRM','addressbook'); $this->tmpl->exec('addressbook.addressbook_ui.view',$content,$sel_options,$readonlys,array( 'id' => $content['id'], diff --git a/addressbook/js/CRM.ts b/addressbook/js/CRM.ts new file mode 100644 index 0000000000..95c002e9ac --- /dev/null +++ b/addressbook/js/CRM.ts @@ -0,0 +1,209 @@ +/** + * EGroupware - Addressbook - Javascript UI + * + * @link: https://www.egroupware.org + * @package addressbook + * @author Hadi Nategh + * @copyright (c) 2008-13 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + +/*egw:uses + /api/js/jsapi/egw_app.js + */ + +import 'jquery'; +import 'jqueryui'; +import '../jsapi/egw_global'; +import '../etemplate/et2_types'; + +import {EgwApp, PushData} from '../../api/js/jsapi/egw_app'; +import {etemplate2} from "../../api/js/etemplate/etemplate2"; +import {et2_nextmatch} from "../../api/js/etemplate/et2_extension_nextmatch"; + +/** + * UI for Addressbook CRM view + * + */ +export class CRMView extends EgwApp +{ + // Reference to the list + nm: et2_nextmatch = null; + + // Which addressbook contact id(s) we are showing entries for + contact_ids: string[] = []; + + // Private js for the list + app_obj: EgwApp = null; + + // Hold on to the original push handler + private _app_obj_push: (pushData: PushData) => void; + + // Push data key(s) to check for our contact ID + private push_contact_ids = ["contact_id"]; + + /** + * Constructor + * + * CRM is part of addressbook + */ + constructor() + { + // call parent + super('addressbook'); + } + + /** + * Destructor + */ + destroy(_app) + { + this.nm = null; + if(this.app_obj != null) + { + this.app_obj.destroy(_app); + } + + // call parent + super.destroy(_app); + } + + /** + * A template from an app is ready, looks like it might be a CRM view. + * Check it, get CRM ready, and bind accordingly + * + * @param et2 + * @param appname + */ + static view_ready(et2: etemplate2, app_obj: EgwApp) + { + // Check to see if the template is for a CRM view + if(et2.app == app_obj.appname) + { + return false; + } + + // Make sure object is there, etemplate2 will pick it up and call our et2_ready + if(typeof et2.app_obj.crm == "undefined" && app.classes.crm) + { + et2.app_obj.crm = new app.classes.crm(); + } + if(typeof et2.app_obj.crm == "undefined") + { + egw.debug("error", "CRMView object is missing"); + return false; + } + + let crm = et2.app_obj.crm; + + // We can set this now + crm.set_view_obj(app_obj); + } + + /** + * This function is called when the etemplate2 object is loaded + * and ready. The associated app [is supposed to have] already called its own et2_ready(), + * so any changes done here will override the app. + * + * @param {etemplate2} et2 newly ready object + * @param {string} name Template name + */ + et2_ready(et2, name) + { + // call parent + super.et2_ready(et2, name); + + } + + /** + * Set the associated private app JS + * We try and pull the needed info here + */ + set_view_obj(app_obj: EgwApp) + { + this.app_obj = app_obj; + + // For easy reference later + this.nm = app_obj.et2.getDOMWidgetById('nm'); + + let contact_ids = app_obj.et2.getArrayMgr("content").getEntry("action_id") || ""; + if(typeof contact_ids == "string") + { + contact_ids = contact_ids.split(","); + } + this.set_contact_ids(contact_ids); + + // Override the push handler + this._override_push(app_obj); + } + + /** + * Set or change which contact IDs we are showing entries for + */ + set_contact_ids(ids: string[]) + { + this.contact_ids = ids; + let filter = {action_id: this.contact_ids}; + + if(this.nm !== null) + { + this.nm.applyFilters(filter); + } + } + + /** + * 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) + { + if(pushData.app !== this.app_obj.appname || !this.nm) return; + + // If we know about it and it's an update, just update. + // This must be before all ACL checks, as contact might have changed and entry needs to be removed + // (server responds then with null / no entry causing the entry to disapear) + if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) + { + return this.nm.refresh(pushData.id, pushData.type); + } + + // Check if it's for one of our contacts + for(let field of this.push_contact_ids) + { + if(pushData.acl && pushData.acl[field]) + { + let val = typeof pushData.acl[field] == "string" ? [pushData.acl[field]] : pushData.acl[field]; + if(val.filter(v => this.contact_ids.indexOf(v) >= 0).length > 0) + { + return this._app_obj_push(pushData); + } + } + } + } + + /** + * Override the list's push handler to do nothing, we'll call it if we want it. + * + * @param app_obj + * @private + */ + _override_push(app_obj : EgwApp) + { + this._app_obj_push = app_obj.push.bind(app_obj); + app_obj.push = function(pushData) {return false;}; + } +} + + +app.classes.crm = CRMView; \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js index c5cb6d9228..17d41a25f4 100644 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ b/api/js/etemplate/et2_extension_nextmatch.js @@ -529,34 +529,48 @@ var et2_nextmatch = /** @class */ (function (_super) { this.dataview.grid.invalidate(); } } - 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]; + var _loop_1 = function () { + var uid_1 = _row_ids[i].toString().indexOf(this_1.controller.dataStorePrefix) == 0 ? _row_ids[i] : this_1.controller.dataStorePrefix + "::" + _row_ids[i]; + // Check for update on a row we don't have + var known = Object.values(this_1.controller._indexMap).filter(function (row) { return row.uid == uid_1; }); + if ((_type == et2_nextmatch.UPDATE || _type == et2_nextmatch.UPDATE_IN_PLACE) && (!known || known.length == 0)) { + _type = et2_nextmatch.ADD; + if (update_pref == "exact" && !this_1.is_sorted_by_modified()) { + _type = et2_nextmatch.EDIT; + } + } switch (_type) { // update-in-place = update, but always only in place case et2_nextmatch.UPDATE_IN_PLACE: - this.egw().dataRefreshUID(uid); + this_1.egw().dataRefreshUID(uid_1); break; // 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; + if (!this_1.refresh_update(uid_1)) { + return "break-id_loop"; } break; case et2_nextmatch.DELETE: // Handled above, more code to execute after loop so don't exit early break; case et2_nextmatch.ADD: - if (update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified()) { - if (this.refresh_add(uid)) + if (update_pref == "lazy" || update_pref == "exact" && this_1.is_sorted_by_modified()) { + if (this_1.refresh_add(uid_1)) break; } // fall-through / full refresh, if refresh_add returns false case et2_nextmatch.EDIT: default: // Trigger refresh - this.applyFilters(); - break id_loop; + this_1.applyFilters(); + return "break-id_loop"; + } + }; + var this_1 = this; + id_loop: for (var i = 0; i < _row_ids.length; i++) { + var state_1 = _loop_1(); + switch (state_1) { + case "break-id_loop": break id_loop; } } // Trigger an event so app code can act on it @@ -571,13 +585,11 @@ var et2_nextmatch = /** @class */ (function (_super) { et2_nextmatch.prototype.refresh_update = function (uid) { // Row data update has been sent, let's move it where app wants it var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + // Ask for new data + this.egw().dataRefreshUID(uid); // Need to delete first as there's a good chance indexes will change in an unknown way // and we can't always find it by UID after due to duplication this.controller.deleteRow(uid); - // Trigger controller to remove from internals so we can ask for new data - this.egw().dataStoreUID(uid, null); - // Stop caring about this ID - this.egw().dataDeleteUID(uid); // 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, et2_nextmatch.UPDATE)) { // App did not want the row, or doesn't know where it goes but we've already removed it... @@ -601,6 +613,7 @@ var et2_nextmatch = /** @class */ (function (_super) { */ et2_nextmatch.prototype.refresh_add = function (uid, type) { if (type === void 0) { type = et2_nextmatch.ADD; } + var _a, _b; var index = egw.preference("lazy-update") == "lazy" ? 0 : (this.is_sorted_by_modified() ? 0 : false); // No add, do a full refresh @@ -614,7 +627,7 @@ var et2_nextmatch = /** @class */ (function (_super) { entry.idx = typeof index == "number" ? index : 0; this.controller._insertDataRow(entry, true); // Set "new entry" class - but it has to stay so register and re-add it after the data is there - entry.row.tr.addClass("new_entry"); + (_b = (_a = entry.row) === null || _a === void 0 ? void 0 : _a.tr) === null || _b === void 0 ? void 0 : _b.addClass("new_entry"); var callback = function (data) { if (data) { if (data.class) { diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index a9a50619fd..e4dedb0efc 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -818,7 +818,18 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 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]; + let uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + + // Check for update on a row we don't have + let known = Object.values(this.controller._indexMap).filter(function(row) {return row.uid ==uid;}); + if((_type == et2_nextmatch.UPDATE || _type == et2_nextmatch.UPDATE_IN_PLACE) && (!known || known.length == 0 )) + { + _type = et2_nextmatch.ADD; + if (update_pref == "exact" && !this.is_sorted_by_modified()) + { + _type = et2_nextmatch.EDIT; + } + } switch(_type) { // update-in-place = update, but always only in place @@ -864,16 +875,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Row data update has been sent, let's move it where app wants it let entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + // Ask for new data + this.egw().dataRefreshUID(uid); + // Need to delete first as there's a good chance indexes will change in an unknown way // and we can't always find it by UID after due to duplication this.controller.deleteRow(uid); - // Trigger controller to remove from internals so we can ask for new data - this.egw().dataStoreUID(uid,null); - - // Stop caring about this ID - this.egw().dataDeleteUID(uid); - // 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, et2_nextmatch.UPDATE)) { @@ -918,7 +926,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.controller._insertDataRow(entry,true); // Set "new entry" class - but it has to stay so register and re-add it after the data is there - entry.row.tr.addClass("new_entry"); + entry.row?.tr?.addClass("new_entry"); + let callback = function(data) { if(data) { diff --git a/infolog/inc/class.infolog_hooks.inc.php b/infolog/inc/class.infolog_hooks.inc.php index 95256808ac..cf9636f37d 100644 --- a/infolog/inc/class.infolog_hooks.inc.php +++ b/infolog/inc/class.infolog_hooks.inc.php @@ -73,10 +73,29 @@ class infolog_hooks 'edit_id' => 'info_id', 'edit_popup' => '760x570', 'merge' => true, - 'push_data' => ['info_type', 'info_owner','info_responsible', 'info_modified'] + 'push_data' => self::class.'::prepareEventPush' ); } + /** + * Prepare entry to be pushed via Link::notify_update() + * + * Get linked contact ID for CRM view + * + * @param $entry + * @return array + */ + static public function prepareEventPush($entry) + { + $info = array_intersect_key($entry, array_flip(['info_type', 'info_owner','info_responsible', 'info_modified'])); + + // Add in contact ID for CRM view + if($entry['info_contact'] && $entry['info_contact']['app'] == 'addressbook') + { + $info['contact_id'] = $entry['info_contact']['id']; + } + return $info; + } /** * hooks to build sidebox-menu plus the admin and preferences sections * diff --git a/infolog/js/app.js b/infolog/js/app.js index 4c47315b15..2db5243cf7 100644 --- a/infolog/js/app.js +++ b/infolog/js/app.js @@ -32,6 +32,7 @@ require("../etemplate/et2_types"); var egw_app_1 = require("../../api/js/jsapi/egw_app"); var etemplate2_1 = require("../../api/js/etemplate/etemplate2"); var et2_extension_nextmatch_1 = require("../../api/js/etemplate/et2_extension_nextmatch"); +var CRM_1 = require("../../addressbook/js/CRM"); /** * UI for Infolog * @@ -70,6 +71,10 @@ var InfologApp = /** @class */ (function (_super) { InfologApp.prototype.et2_ready = function (_et2, _name) { // call parent _super.prototype.et2_ready.call(this, _et2, _name); + // CRM View + if (typeof CRM_1.CRMView !== "undefined") { + CRM_1.CRMView.view_ready(_et2, this); + } switch (_name) { case 'infolog.index': this.filter_change(); @@ -178,7 +183,7 @@ var InfologApp = /** @class */ (function (_super) { // This must be before all ACL checks, as responsible might have changed and entry need to be removed // (server responds then with null / no entry causing the entry to disapear) if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) { - return etemplate2_1.etemplate2.app_refresh("", pushData.app, pushData.id, pushData.type); + return this.et2.getInstanceManager().refresh("", pushData.app, pushData.id, pushData.type); } // check visibility - grants is ID => permission of people we're allowed to see if (typeof this._grants === 'undefined') { diff --git a/infolog/js/app.ts b/infolog/js/app.ts index 3787dbf08d..5457c89dce 100644 --- a/infolog/js/app.ts +++ b/infolog/js/app.ts @@ -21,6 +21,7 @@ import {EgwApp} from '../../api/js/jsapi/egw_app'; import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog"; import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {et2_nextmatch} from "../../api/js/etemplate/et2_extension_nextmatch"; +import {CRMView} from "../../addressbook/js/CRM"; /** * UI for Infolog @@ -66,6 +67,12 @@ class InfologApp extends EgwApp // call parent super.et2_ready(_et2, _name); + // CRM View + if(typeof CRMView !== "undefined") + { + CRMView.view_ready(_et2, this); + } + switch(_name) { case 'infolog.index': @@ -188,7 +195,7 @@ class InfologApp extends EgwApp // (server responds then with null / no entry causing the entry to disapear) if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) { - return etemplate2.app_refresh("", pushData.app, pushData.id, pushData.type); + return this.et2.getInstanceManager().refresh("", pushData.app, pushData.id, pushData.type); } // check visibility - grants is ID => permission of people we're allowed to see