diff --git a/api/js/etemplate/et2_core_legacyJSFunctions.ts b/api/js/etemplate/et2_core_legacyJSFunctions.ts index 0f46665b2a..87d6aee233 100644 --- a/api/js/etemplate/et2_core_legacyJSFunctions.ts +++ b/api/js/etemplate/et2_core_legacyJSFunctions.ts @@ -17,6 +17,7 @@ import {egw} from "../jsapi/egw_global"; import {et2_IDOMNode} from "./et2_core_interfaces"; +import {et2_form_name} from "./et2_core_common"; export function et2_compileLegacyJS(_code, _widget, _context) { @@ -144,7 +145,8 @@ function js_pseudo_funcs(_val,widget) { // et2_form_name doesn't care about ][, just [ var _cname = widget.getPath() ? widget.getPath().join("[") : false; - _val = _val.replace(/form::name\(/g, "'"+widget.getRoot()._inst.uniqueId+"_'+"+(_cname ? "et2_form_name('"+_cname+"'," : '(')); + document.et2_form_name = et2_form_name; + _val = _val.replace(/form::name\(/g, "'"+widget.getRoot()._inst.uniqueId+"_'+"+(_cname ? "document.et2_form_name('"+_cname+"'," : '(')); } if (_val.indexOf('egw::lang(') != -1) diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index 66acfc7be1..edd1e53402 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -74,7 +74,8 @@ import Sortable from 'sortablejs/modular/sortable.complete.esm.js'; /** * Interface all special nextmatch header elements have to implement. */ -export interface et2_INextmatchHeader { +export interface et2_INextmatchHeader +{ /** * The 'setNextmatch' function is called by the parent nextmatch widget @@ -85,6 +86,7 @@ export interface et2_INextmatchHeader { */ setNextmatch(nextmatch : et2_nextmatch) : void } + export const et2_INextmatchHeader = "et2_INextmatchHeader"; et2_implements_registry.et2_INextmatchHeader = function(obj : et2_widget) { @@ -93,11 +95,11 @@ et2_implements_registry.et2_INextmatchHeader = function(obj : et2_widget) export interface et2_INextmatchSortable { - setSortmode(_sort_mode): void + setSortmode(_sort_mode) : void } export const et2_INextmatchSortable = "et2_INextmatchSortable"; -et2_implements_registry.et2_INextmatchSortable = function(obj: et2_widget) +et2_implements_registry.et2_INextmatchSortable = function(obj : et2_widget) { return implements_methods(obj, ["setSortmode"]); } @@ -105,9 +107,9 @@ et2_implements_registry.et2_INextmatchSortable = function(obj: et2_widget) // For holding settings while whe print interface PrintSettings { - old_height: number, - row_selector: string, - orientation_style: HTMLStyleElement + old_height : number, + row_selector : string, + orientation_style : HTMLStyleElement } interface ActiveFilters @@ -115,9 +117,9 @@ interface ActiveFilters search? : string, filter? : any, filter2? : any, - col_filter: {[key:string]: any}, - selectcols?: string[], - searchletter?: string, + col_filter : { [key : string] : any }, + selectcols? : string[], + searchletter? : string, selected? : string[] } @@ -235,7 +237,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 /** * 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'; @@ -243,49 +245,49 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 public static readonly DELETE = 'delete'; // DOM / jQuery stuff - private div: JQuery; - private innerDiv: JQuery; - private dynheight: any; - private blank: JQuery; + private div : JQuery; + private innerDiv : JQuery; + private dynheight : any; + private blank : JQuery; // Popup to select columns - private selectPopup: any; + private selectPopup : any; public static readonly legacyOptions = ["template", "hide_header", "header_left", "header_right"]; - private template: any; - columns: { visible: boolean, widget: et2_widget }[]; - private sortedColumnsList: string[]; + private template : any; + columns : { visible : boolean, widget : et2_widget }[]; + private sortedColumnsList : string[]; // If we need the nextmatch to have a value, keep it here. // Normally this is used in actions, and is the action and selected rows. - private value: any; + private value : any; // Big old bag of settings - private settings: any; + private settings : any; // Current view, either row or tile. We store it here as controllers are // recreated when the template changes. - view: string; + view : string; // Sub-objects used for actual work - private readonly header: et2_nextmatch_header_bar; - dataview: any; - controller: any; - private rowProvider: any; + private readonly header : et2_nextmatch_header_bar; + dataview : any; + controller : any; + private rowProvider : any; // Flag for an update is currently being done, to avoid a loop - private update_in_progress: boolean; + private update_in_progress : boolean; // Window timer for automatically refreshing - private _autorefresh_timer: number; + private _autorefresh_timer : number; // Nextmatch can't render while hidden, we store refresh requests for later - private _queued_refreshes : null|{type : string, ids: string[]}[] = []; + private _queued_refreshes : null | { type : string, ids : string[] }[] = []; // When printing, we change the layout around. Keep some values so it can be restored after - private print: PrintSettings = { + private print : PrintSettings = { old_height: 0, row_selector: '', orientation_style: null @@ -296,11 +298,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @memberOf et2_nextmatch */ - constructor(_parent?, _attrs?: WidgetConfig, _child?: object) + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})); - this.activeFilters = {col_filter:{}}; + this.activeFilters = {col_filter: {}}; this.columns = []; // keeps sorted columns this.sortedColumnsList = []; @@ -331,7 +333,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 .addClass("et2_nextmatch"); - this.header = et2_createWidget("nextmatch_header_bar", {}, this); + this.header = et2_createWidget("nextmatch_header_bar", {}, this); this.innerDiv = jQuery(document.createElement("div")) .appendTo(this.div); @@ -397,12 +399,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { super.transformAttributes(_attrs); - if (this.id) + if(this.id) { const entry = this.getArrayMgr("content").data; _attrs["settings"] = {}; - if (entry) + if(entry) { _attrs["settings"] = entry; @@ -448,7 +450,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Register a handler // @ts-ignore jQuery(this.div) - .on('dragenter','.egwGridView_grid tr',function(e) { + .on('dragenter', '.egwGridView_grid tr', function(e) + { // Figure out _which_ row const row = self.controller.getRowByNode(this); @@ -456,39 +459,44 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { return false; } - e.stopPropagation(); e.preventDefault(); + e.stopPropagation(); + e.preventDefault(); // Indicate acceptance if(row.controller && row.controller._selectionMgr) { - row.controller._selectionMgr.setFocused(row.uid,true); + row.controller._selectionMgr.setFocused(row.uid, true); } return false; }) - .on('dragexit','.egwGridView_grid tr', function() { + .on('dragexit', '.egwGridView_grid tr', function() + { self.controller._selectionMgr.setFocused(); }) - .on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy") + .on('dragover', '.egwGridView_grid tr', false).attr("dropzone", "copy") - .on('drop', '.egwGridView_grid tr',function(e) { - self.handle_drop(e,this); + .on('drop', '.egwGridView_grid tr', function(e) + { + self.handle_drop(e, this); return false; }); } } // stop invalidation in no visible tabs - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function() { + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function() + { if(this.controller && this.controller._grid) { this.controller._grid.doInvalidate = false; } - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function() { + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function() + { if(this.controller && this.controller._grid) { this.controller._grid.doInvalidate = true; } - },this)); + }, this)); return true; } @@ -499,9 +507,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 */ resize() { - if (this.dynheight) + if(this.dynheight) { - this.dynheight.update(function(_w, _h) { + this.dynheight.update(function(_w, _h) + { this.dataview.resize(_w, _h); }, this); } @@ -516,16 +525,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * automatically. * @param {boolean} _update true/undefined: call applyFilters, false: only set sort */ - sortBy( _id, _asc, _update? : boolean) + sortBy(_id, _asc, _update? : boolean) { - if (typeof _update == "undefined") + if(typeof _update == "undefined") { _update = true; } // Create the "sort" entry in the active filters if it did not exist // yet. - if (typeof this.activeFilters["sort"] == "undefined") + if(typeof this.activeFilters["sort"] == "undefined") { this.activeFilters["sort"] = { "id": null, @@ -534,23 +543,24 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Determine the sort direction automatically if it is not set - if (typeof _asc == "undefined") + if(typeof _asc == "undefined") { _asc = true; - if (this.activeFilters["sort"].id == _id) + if(this.activeFilters["sort"].id == _id) { _asc = !this.activeFilters["sort"].asc; } } // Set the sortmode display - this.iterateOver(function(_widget) { - _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none"); + this.iterateOver(function(_widget) + { + _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc" : "desc") : "none"); }, this, et2_INextmatchSortable); - if (_update) + if(_update) { - this.applyFilters({sort: { id: _id, asc: _asc}}); + this.applyFilters({sort: {id: _id, asc: _asc}}); } else { @@ -569,10 +579,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 resetSort() { // Check whether the nextmatch widget is currently sorted - if (typeof this.activeFilters["sort"] != "undefined") + if(typeof this.activeFilters["sort"] != "undefined") { // Reset the sort mode - this.iterateOver(function(_widget) { + this.iterateOver(function(_widget) + { _widget.setSortmode("none"); }, this, et2_INextmatchSortable); @@ -586,7 +597,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header */ - applyFilters( _set? : object | any) + applyFilters(_set? : object | any) { let changed = false; let keep_selection = false; @@ -610,15 +621,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.activeFilters.col_filter = {}; } - if (typeof _set == 'object') + if(typeof _set == 'object') { for(let s in _set) { - if (s == 'col_filter') + if(s == 'col_filter') { // allow apps setState() to reset all col_filter by using undefined or null for it // they can not pass {} for _set / state.state, if they need to set something - if (_set.col_filter === undefined || _set.col_filter === null) + if(_set.col_filter === undefined || _set.col_filter === null) { this.activeFilters.col_filter = {}; changed = true; @@ -627,9 +638,9 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { for(let c in _set.col_filter) { - if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) + if(this.activeFilters.col_filter[c] !== _set.col_filter[c]) { - if (_set.col_filter[c]) + if(_set.col_filter[c]) { this.activeFilters.col_filter[c] = _set.col_filter[c]; } @@ -642,7 +653,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } } } - else if (s === 'selected') + else if(s === 'selected') { changed = true; keep_selection = true; @@ -650,11 +661,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.controller._objectManager.clear(); for(let i in _set.selected) { - this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true); + this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::' + _set.selected[i], true); } delete _set.selected; } - else if (this.activeFilters[s] !== _set[s]) + else if(this.activeFilters[s] !== _set[s]) { this.activeFilters[s] = _set[s]; changed = true; @@ -672,7 +683,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 else { // Do not keep selection - this.controller._selectionMgr.resetSelection(); + this.controller._selectionMgr.resetSelection(); this.controller._objectManager.clear(); this.controller.keepSelection(); } @@ -684,7 +695,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.header.setFilters(this.activeFilters); // Update any column filters - this.iterateOver(function(column) { + this.iterateOver(function(column) + { // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return; @@ -692,7 +704,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); } - if (column.id && typeof column.get_value == "function") + if(column.id && typeof column.get_value == "function") { this[column.id] = column.get_value(); } @@ -747,7 +759,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * @see egw_app.nm_refresh_index() * @fires refresh from the widget itself */ - refresh( _row_ids, _type) + refresh(_row_ids, _type) { // Framework trying to refresh, but nextmatch not fully initialized if(this.controller === null || !this.div) @@ -756,7 +768,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Make sure we're dealing with arrays - if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; + if(typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; // Make some changes in what we're doing based on preference let update_pref = egw.preference("lazy-update") || 'lazy'; @@ -764,35 +776,36 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { _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()) + else if(update_pref == "exact" && _type == et2_nextmatch.ADD && !this.is_sorted_by_modified()) { _type = et2_nextmatch.EDIT; } - if( _type == et2_nextmatch.ADD && !( update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified() )) + if(_type == et2_nextmatch.ADD && !(update_pref == "lazy" || update_pref == "exact" && this.is_sorted_by_modified())) { _type = et2_nextmatch.EDIT; } - if (typeof _type == 'undefined') _type = et2_nextmatch.EDIT; + if(typeof _type == 'undefined') _type = et2_nextmatch.EDIT; - if (!this.div.is(':visible')) // run refresh, once we become visible again + if(!this.div.is(':visible')) // run refresh, once we become visible again { return this._queue_refresh(_row_ids, _type); } - if (typeof _row_ids == "undefined" || _row_ids === null) + if(typeof _row_ids == "undefined" || _row_ids === null) { this.applyFilters(); // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this]); + jQuery(this).triggerHandler("refresh", [this]); return; } // Clean IDs in case they're UIDs with app prefixed - _row_ids = _row_ids.map( function(id) { - if( id.toString().indexOf(this.controller.dataStorePrefix) == -1) + _row_ids = _row_ids.map(function(id) + { + if(id.toString().indexOf(this.controller.dataStorePrefix) == -1) { return id; } @@ -816,7 +829,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Stop automatic updating this.dataview.grid.doInvalidate = false; - for (var i = 0; i < _row_ids.length; i++) + for(var i = 0; i < _row_ids.length; i++) { uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; @@ -845,79 +858,82 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } id_loop: - for(var i = 0; i < _row_ids.length; 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 )) + for(var i = 0; i < _row_ids.length; i++) { - _type = et2_nextmatch.ADD; - if (update_pref == "exact" && !this.is_sorted_by_modified()) + 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) { - _type = et2_nextmatch.EDIT; + 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; + } + } + if([et2_nextmatch.ADD, et2_nextmatch.UPDATE].indexOf(_type) !== -1) + { + // Pre-ask for the row data, and only proceed if we actually get it + // need to send nextmatch filters too, as server-side will merge old version from request otherwise + this.egw().dataFetch( + this.getInstanceManager().etemplate_exec_id, + {refresh: _row_ids}, + this.controller._filters, + this.id, function(data) + { + // In the event that the etemplate got removed before the data came back (Usually an action caused + // a full submit) just stop here. + if(!this.nm.getParent()) return; + + if(data.total >= 1) + { + this.type == et2_nextmatch.ADD ? this.nm.refresh_add(this.uid, this.type) + : this.nm.refresh_update(this.uid); + } + else if(this.type == et2_nextmatch.UPDATE) + { + // Remove row from controller + this.nm.controller.deleteRow(this.uid); + + // Adjust total rows, clean grid + this.nm.controller._grid.setTotalCount(this.nm.controller._grid._total - _row_ids.length); + this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid._total); + } + }, {type: _type, nm: this, uid: uid, prefix: this.controller.dataStorePrefix}, [_row_ids] + ); + return; + } + switch(_type) + { + // update-in-place = update, but always only in place + case et2_nextmatch.UPDATE_IN_PLACE: + this.egw().dataRefreshUID(uid); + break; + + // These ones handled above in dataFetch() callback + case et2_nextmatch.UPDATE: + // update [existing] row, maybe we'll put it on top + break; + case et2_nextmatch.DELETE: + // Handled above, more code to execute after loop so don't exit early + break; + case et2_nextmatch.ADD: + break; + + // No more smart things we can do, refresh the whole thing + case et2_nextmatch.EDIT: + default: + // Trigger refresh + this.applyFilters(); + break id_loop; } } - if([et2_nextmatch.ADD, et2_nextmatch.UPDATE].indexOf(_type) !== -1) - { - // Pre-ask for the row data, and only proceed if we actually get it - // need to send nextmatch filters too, as server-side will merge old version from request otherwise - this.egw().dataFetch( - this.getInstanceManager().etemplate_exec_id, - {refresh:_row_ids}, - this.controller._filters, - this.id, function(data) - { - // In the event that the etemplate got removed before the data came back (Usually an action caused - // a full submit) just stop here. - if(!this.nm.getParent()) return; - - if(data.total >= 1) - { - this.type == et2_nextmatch.ADD ? this.nm.refresh_add(this.uid, this.type) - : this.nm.refresh_update(this.uid); - } - else if (this.type == et2_nextmatch.UPDATE) - { - // Remove row from controller - this.nm.controller.deleteRow(this.uid); - - // Adjust total rows, clean grid - this.nm.controller._grid.setTotalCount(this.nm.controller._grid._total- _row_ids.length); - this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid._total); - } - }, {type: _type, nm: this, uid: uid, prefix: this.controller.dataStorePrefix}, [_row_ids] - ); - return; - } - switch(_type) - { - // update-in-place = update, but always only in place - case et2_nextmatch.UPDATE_IN_PLACE: - this.egw().dataRefreshUID(uid); - break; - - // These ones handled above in dataFetch() callback - case et2_nextmatch.UPDATE: - // update [existing] row, maybe we'll put it on top - break; - case et2_nextmatch.DELETE: - // Handled above, more code to execute after loop so don't exit early - break; - case et2_nextmatch.ADD: - break; - - // No more smart things we can do, refresh the whole thing - case et2_nextmatch.EDIT: - default: - // Trigger refresh - this.applyFilters(); - break id_loop; - } - } // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]); + jQuery(this).triggerHandler("refresh", [this, _row_ids, _type]); } /** @@ -926,7 +942,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param uid */ - protected refresh_update(uid: string) + protected refresh_update(uid : string) { // Row data update has been sent, let's move it where app wants it let entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); @@ -940,15 +956,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { // 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. - let callback = function(data) { + let callback = function(data) + { data.class += " new_entry"; this.egw().dataUnregisterUID(uid, callback, this); }; this.egw().dataRegisterUID(uid, callback, this, this.getInstanceManager().etemplate_exec_id, this.id); - this.controller._insertDataRow(entry,true); + this.controller._insertDataRow(entry, true); } // Update does not need to increase row count, but refresh_add() adds it in - this.controller._grid.setTotalCount(this.controller._grid.getTotalCount()-1); + this.controller._grid.setTotalCount(this.controller._grid.getTotalCount() - 1); this.controller._selectionMgr.setTotalCount(this.controller._grid.getTotalCount()); return true; @@ -960,10 +977,10 @@ 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 = et2_nextmatch.ADD) + protected refresh_add(uid : string, type = et2_nextmatch.ADD) { let index : boolean | number = egw.preference("lazy-update") !== "exact" ? 0 : - (this.is_sorted_by_modified() ? 0 : false); + (this.is_sorted_by_modified() ? 0 : false); // No add, do a full refresh if(index === false) @@ -973,7 +990,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let time = new Date().valueOf(); - this.egw().dataRegisterUID(uid, this._push_add_callback, {nm: this, uid: uid, index: index}, this.getInstanceManager().etemplate_exec_id, this.id); + this.egw().dataRegisterUID(uid, this._push_add_callback, { + nm: this, + uid: uid, + index: index + }, this.getInstanceManager().etemplate_exec_id, this.id); return true; } @@ -982,7 +1003,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * Expected context: {nm: this, uid: string, index: number} */ - protected _push_add_callback(this: {nm: et2_nextmatch, uid: string, index: number}, data:any) + protected _push_add_callback(this : { nm : et2_nextmatch, uid : string, index : number }, data : any) { if(data && this.nm && this.nm.getParent()) { @@ -995,20 +1016,20 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 //if(stored?.timestamp >= time) return; // Increase displayed row count or we lose the last row when we add and the total is wrong - this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount()+1); + this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount() + 1); this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid.getTotalCount()); // Insert at the top of the list, or where app said var entry = this.nm.controller._selectionMgr._getRegisteredRowsEntry(this.uid); entry.idx = typeof this.index == "number" ? this.index : 0; - this.nm.controller._insertDataRow(entry,true); + this.nm.controller._insertDataRow(entry, true); } - else if (this.nm && this.nm.getParent()) + else if(this.nm && this.nm.getParent()) { // Server didn't give us our row data // Delete from internal references this.nm.controller.deleteRow(this.uid); - this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount()-1); + this.nm.controller._grid.setTotalCount(this.nm.controller._grid.getTotalCount() - 1); this.nm.controller._selectionMgr.setTotalCount(this.nm.controller._grid.getTotalCount()); } this.nm.egw().dataUnregisterUID(this.uid, this.nm._push_add_callback, this); @@ -1036,9 +1057,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 return; } - // Cancel any existing listener + // Bind so we can get the queued data when tab is re-activated let tab = jQuery(this.getInstanceManager().DOMContainer.parentNode) - .off('show.et2_nextmatch') .one('show.et2_nextmatch', this._queue_refresh_callback.bind(this)); @@ -1051,7 +1071,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Skip if already in array - if(this._queued_refreshes.some( queue => queue.ids.length === _row_ids.length && queue.ids.every((value, index) => value === _row_ids[index]))) + if(this._queued_refreshes.some(queue => queue.ids.length === _row_ids.length && queue.ids.every((value, index) => value === _row_ids[index]))) { return; } @@ -1119,14 +1139,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @return Object { ids: [UIDs], inverted: boolean} */ - getSelection() : {ids : string[], all : boolean} + getSelection() : { ids : string[], all : boolean } { const selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; if(typeof selected == "object" && selected != null) { return selected; } - return {ids:[],all:false}; + return {ids: [], all: false}; } /** @@ -1136,7 +1156,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { let guts = function(controller) { - console.log("Controller:",controller); + console.log("Controller:", controller); console.log("Controller indexMap:", controller._indexMap); console.log("Grid:", controller._grid); console.log("Selection Manager:", controller._selectionMgr); @@ -1172,10 +1192,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * @param action ActionObject From action system. Ignored. * @param senders ActionObjectImplemetation From action system. Ignored. */ - onselect( action,senders) + onselect(action, senders) { // Execute the JS code connected to the event handler - if (typeof this.options.onselect == 'function') + if(typeof this.options.onselect == 'function') { return this.options.onselect.call(this, this.getSelection().ids, this); } @@ -1184,7 +1204,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 /** * Nextmatch needs a namespace */ - protected _createNamespace(): boolean + protected _createNamespace() : boolean { return true; } @@ -1204,7 +1224,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); } - else if (tab && tab.contentDiv) + else if(tab && tab.contentDiv) { return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); } @@ -1217,17 +1237,18 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {et2_widget} _widget */ - _genColumnCaption( _widget) + _genColumnCaption(_widget) { let result = null; if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption(); const self = this; - _widget.iterateOver(function(_widget) { + _widget.iterateOver(function(_widget) + { const label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); - if (!label) return; // skip empty, undefined or null labels - if (!result) + if(!label) return; // skip empty, undefined or null labels + if(!result) { result = label; } @@ -1248,19 +1269,21 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {et2_widget} _widget */ - _getColumnName( _widget) + _getColumnName(_widget) { if(typeof _widget._getColumnName == 'function') return _widget._getColumnName(); const name = _widget.id; const child_names = []; const children = _widget.getChildren(); - for(let i = 0; i < children.length; i++) { + for(let i = 0; i < children.length; i++) + { if(children[i].id) child_names.push(children[i].id); } const colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); - if(colName == "") { + if(colName == "") + { this.egw().debug("info", "Unable to generate nm column name for ", _widget); } return colName; @@ -1294,19 +1317,20 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let app = ''; let list = []; - if(this.options.settings.columnselection_pref) { + if(this.options.settings.columnselection_pref) + { let pref = {}; list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { - app = list[0].substring('nextmatch'.length+1); + app = list[0].substring('nextmatch'.length + 1); pref = egw.preference(this.options.settings.columnselection_pref, app); } else { app = list[0]; // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); } if(pref) { @@ -1317,14 +1341,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let columnDisplay = []; // If no column preference or default set, use all columns - if(typeof columnPreference =="string" && columnPreference.length == 0) + if(typeof columnPreference == "string" && columnPreference.length == 0) { columnDisplay = []; negated = true; } columnDisplay = typeof columnPreference === "string" - ? et2_csvSplit(columnPreference,null,",") : columnPreference; + ? et2_csvSplit(columnPreference, null, ",") : columnPreference; // Adjusted column sizes let size = {}; @@ -1335,7 +1359,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // If columnselection pref is missing prefix, add it in if(size_pref.indexOf('nextmatch') == -1) { - size_pref = 'nextmatch-'+size_pref; + size_pref = 'nextmatch-' + size_pref; } size = this.egw().preference(size_pref, app); } @@ -1362,7 +1386,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * @param {array} _row * @param {array} _colData */ - _applyUserPreferences( _row, _colData) + _applyUserPreferences(_row, _colData) { const prefs = this._getPreferences(); const columnDisplay = prefs.visible; @@ -1375,88 +1399,91 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 if(columnDisplay && columnDisplay.length > 0) { RowLoop: - for(let i = 0; i < _row.length; i++) - { - colName = ''; - if(_row[i].disabled === true) + for(let i = 0; i < _row.length; i++) { - _colData[i].visible = false; - continue; - } - - // Customfields needs special processing - if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) - { - // Find cf field - for(var j = 0; j < columnDisplay.length; j++) - { - if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) { - _row[i].widget.options.fields = {}; - for(let k = j; k < columnDisplay.length; k++) - { - if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) - { - _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; - } - } - // Resets field visibility too - _row[i].widget._getColumnName(); - _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); - break; - } - } - // Disable if there are no custom fields - if(jQuery.isEmptyObject(_row[i].widget.customfields)) + colName = ''; + if(_row[i].disabled === true) { _colData[i].visible = false; continue; } - colName = _row[i].widget.id; - } - else - { - colName = this._getColumnName(_row[i].widget); - } - if(!negated) - { - _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; - } - if(!colName) continue; - _colData[i].visible = negated; - let stop = false; - for(var j = 0; j < columnDisplay.length && !stop; j++) - { - if(columnDisplay[j] == colName) - { - _colData[i].visible = !negated; - stop = true; - } - } - if(size[colName]) - { - // Make sure percentages stay percentages, and forget any preference otherwise - if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") + // Customfields needs special processing + if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) { - _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + // Find cf field + for(var j = 0; j < columnDisplay.length; j++) + { + if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) + { + _row[i].widget.options.fields = {}; + for(let k = j; k < columnDisplay.length; k++) + { + if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) + { + _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; + } + } + // Resets field visibility too + _row[i].widget._getColumnName(); + _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); + break; + } + } + // Disable if there are no custom fields + if(jQuery.isEmptyObject(_row[i].widget.customfields)) + { + _colData[i].visible = false; + continue; + } + colName = _row[i].widget.id; } else { - _colData[i].width = parseInt(size[colName])+'px'; + colName = this._getColumnName(_row[i].widget); + } + if(!negated) + { + _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; + } + if(!colName) continue; + _colData[i].visible = negated; + let stop = false; + for(var j = 0; j < columnDisplay.length && !stop; j++) + { + if(columnDisplay[j] == colName) + { + _colData[i].visible = !negated; + stop = true; + } + } + + if(size[colName]) + { + // Make sure percentages stay percentages, and forget any preference otherwise + if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") + { + _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + } + else + { + _colData[i].width = parseInt(size[colName]) + 'px'; + } } } - } } - _colData.sort(function(a,b) { + _colData.sort(function(a, b) + { return a.order - b.order; }); - _row.sort(function(a,b) { + _row.sort(function(a, b) + { if(typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') { return a.colData.order - b.colData.order; } - else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') + else if(typeof a.order !== 'undefined' && typeof b.order !== 'undefined') { return a.order - b.order; } @@ -1471,7 +1498,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { const colMgr = this.dataview.getColumnMgr(); let app = ""; - if(!this.options.settings.columnselection_pref) { + if(!this.options.settings.columnselection_pref) + { this.options.settings.columnselection_pref = this.options.template; } @@ -1486,14 +1514,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // @ts-ignore const widget = this.columns[i].widget; let colName = this._getColumnName(widget); - if(colName) { + if(colName) + { // Server side wants each cf listed as a seperate column if(widget.instanceOf(et2_nextmatch_customfields)) { // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields colName = widget.id; - for(let name in widget.options.fields) { - if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.PREFIX+name); + for(let name in widget.options.fields) + { + if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.PREFIX + name); } } if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName); @@ -1504,11 +1534,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; } - else if (colMgr.columns[i].fixedWidth) + else if(colMgr.columns[i].fixedWidth) { colSize[colName] = colMgr.columns[i].fixedWidth; } - } else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { + } + else if(colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) + { this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); } } @@ -1517,13 +1549,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let pref = this.options.settings.columnselection_pref; if(pref.indexOf('nextmatch') == 0) { - app = list[0].substring('nextmatch'.length+1); + app = list[0].substring('nextmatch'.length + 1); } else { app = list[0]; // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = "nextmatch-"+this.options.settings.columnselection_pref; + pref = "nextmatch-" + this.options.settings.columnselection_pref; } // Server side wants each cf listed as a seperate column @@ -1537,13 +1569,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // We don't need to re-query if they've removed a column const changed = []; ColLoop: - for(var i = 0; i < colDisplay.length; i++) - { - for(let j = 0; j < oldCols.length; j++) { - if(colDisplay[i] == oldCols[j]) continue ColLoop; + for(var i = 0; i < colDisplay.length; i++) + { + for(let j = 0; j < oldCols.length; j++) + { + if(colDisplay[i] == oldCols[j]) continue ColLoop; + } + changed.push(colDisplay[i]); } - changed.push(colDisplay[i]); - } // If a custom field column was added, throw away cache to deal with // efficient apps that didn't send all custom fields in the first request @@ -1556,13 +1589,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), // Use callback after the preference gets set to trigger refresh, in case app // isn't looking at selectcols and just uses preference - cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null + cf_added ? jQuery.proxy(function() + { + if(this.controller) this.controller.update(true); + }, this) : null ); // Save adjusted column sizes and inform user about it - this.egw().set_preference(app, pref+"-size", colSize); + this.egw().set_preference(app, pref + "-size", colSize); this.egw().message(this.egw().lang("Saved column sizes to preferences.")); } - this.egw().set_preference(app, pref+"-size", colSize); + this.egw().set_preference(app, pref + "-size", colSize); // No significant change (just normal columns shown) and no need to wait, // but the grid still needs to be redrawn if a custom field was removed because @@ -1571,12 +1607,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 if((changed.length || custom_fields.length) && !cf_added) this.applyFilters(); } - _parseHeaderRow( _row, _colData) + _parseHeaderRow(_row, _colData) { // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget - for (var x = 0; x < _row.length; x++) + for(var x = 0; x < _row.length; x++) { if(!_row[x].widget) { @@ -1594,17 +1630,17 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // No action columns in et2 let remove_action_index = null; - for (var x = 0; x < _row.length; x++) + for(var x = 0; x < _row.length; x++) { this.columns[x] = jQuery.extend({ "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, "widget": _row[x].widget - },_colData[x]); + }, _colData[x]); let visibility = (!_colData[x] || _colData[x].visible) ? - et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : - et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; - if(_colData[x].disabled && _colData[x].disabled !=='' && + et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_column.ET2_COL_VISIBILITY_INVISIBLE; + if(_colData[x].disabled && _colData[x].disabled !== '' && this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) { visibility = et2_dataview_column.ET2_COL_VISIBILITY_DISABLED; @@ -1640,7 +1676,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 remove_action_index = x; } - else if (!colName) + else if(!colName) { // Unnamed column cannot be toggled or saved columnData[x].visibility = et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; @@ -1652,15 +1688,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Remove action column if(remove_action_index != null) { - this.columns.splice(remove_action_index,remove_action_index); - columnData.splice(remove_action_index,remove_action_index); - _colData.splice(remove_action_index,remove_action_index); + this.columns.splice(remove_action_index, remove_action_index); + columnData.splice(remove_action_index, remove_action_index); + _colData.splice(remove_action_index, remove_action_index); } // Create the column manager and update the grid container this.dataview.setColumns(columnData); - for (var x = 0; x < _row.length; x++) + for(var x = 0; x < _row.length; x++) { // Append the widget to this container this.addChild(_row[x].widget); @@ -1672,12 +1708,16 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Register handler to update preferences when column properties are changed const self = this; - this.dataview.onUpdateColumns = function() { + this.dataview.onUpdateColumns = function() + { // Use apply to make sure context is there self._updateUserPreferences.apply(self); // Allow column widgets a chance to resize - self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable); + self.iterateOver(function(widget) + { + widget.resize(); + }, self, et2_IResizeable); }; // Register handler for column selection popup, or disable @@ -1688,28 +1728,32 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } if(this.options.settings.no_columnselection) { - this.dataview.selectColumnsClick = function() {return false;}; - jQuery('span.selectcols',this.dataview.headTr).hide(); + this.dataview.selectColumnsClick = function() + { + return false; + }; + jQuery('span.selectcols', this.dataview.headTr).hide(); } else { - jQuery('span.selectcols',this.dataview.headTr).show(); - this.dataview.selectColumnsClick = function(event) { + jQuery('span.selectcols', this.dataview.headTr).show(); + this.dataview.selectColumnsClick = function(event) + { self._selectColumnsClick(event); }; } } - _parseDataRow( _row, _rowData, _colData) + _parseDataRow(_row, _rowData, _colData) { const columnWidgets = []; - _row.sort(function (a, b) - { - return a.colData.order - b.colData.order; - }); + _row.sort(function(a, b) + { + return a.colData.order - b.colData.order; + }); - for (let x = 0; x < this.columns.length; x++) + for(let x = 0; x < this.columns.length; x++) { if(!this.columns[x].visible) { @@ -1759,7 +1803,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { // Use jsapi data module to update let list = this.options.settings.get_rows.split('.', 2); - if (list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" + if(list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" this.options.settings.dataStorePrefix = list[0]; } this.controller.setPrefix(this.options.settings.dataStorePrefix); @@ -1774,7 +1818,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Set the initial row count var total = typeof this.options.settings.total != "undefined" ? - this.options.settings.total : 0; + this.options.settings.total : 0; // This triggers an invalidate, which updates the grid this.dataview.grid.setTotalCount(total); @@ -1791,26 +1835,26 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } } - _parseGrid( _grid) + _parseGrid(_grid) { // Search the rows for a header-row - if one is found, parse it - for (let y = 0; y < _grid.rowData.length; y++) + for(let y = 0; y < _grid.rowData.length; y++) { // Parse the first row as a header, need header to parse the data rows - if (_grid.rowData[y]["class"] == "th" || y == 0) + if(_grid.rowData[y]["class"] == "th" || y == 0) { this._parseHeaderRow(_grid.cells[y], _grid.colData); } else { this._parseDataRow(_grid.cells[y], _grid.rowData[y], - _grid.colData); + _grid.colData); } } this.dataview.table.resize(); } - _getSubgrid( _row, _data, _controller) + _getSubgrid(_row, _data, _controller) { // Fetch the id of the element described by _data, this will be the // parent_id of the elements in the subgrid @@ -1835,27 +1879,29 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 controller.update(); // Register inside the destruction callback of the grid - grid.setDestroyCallback(function () { + grid.setDestroyCallback(function() + { controller.destroy(); }); return grid; } - _getInitialOrder( _rows, _rowId) + _getInitialOrder(_rows, _rowId) { const _order = []; // Get the length of the non-numerical rows arra let len = 0; - for (let key in _rows) { - if (!isNaN(parseInt(key)) && parseInt(key) > len) + for(let key in _rows) + { + if(!isNaN(parseInt(key)) && parseInt(key) > len) len = parseInt(key); } // Iterate over the rows - for (let i = 0; i < len; i++) + for(let i = 0; i < len; i++) { // Get the uid from the data const uid = this.egw().app_name() + '::' + _rows[i][_rowId]; @@ -1870,7 +1916,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 return _order; } - _selectColumnsClick( e) + _selectColumnsClick(e) { const self = this; const columnMgr = this.dataview.getColumnMgr(); @@ -1881,7 +1927,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 const columns = {}; const columns_selected = []; - for (var i = 0; i < columnMgr.columns.length; i++) + for(var i = 0; i < columnMgr.columns.length; i++) { var col = columnMgr.columns[i]; const widget = this.columns[i].widget; @@ -1902,14 +1948,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 if(jQuery.isEmptyObject((widget).customfields)) { // No customfields defined, don't show column - delete(columns[col.id]); + delete (columns[col.id]); continue; } for(var field_name in (widget).customfields) { - columns[et2_nextmatch_customfields.PREFIX+field_name] = " - "+ + columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + (widget).customfields[field_name].label; - if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.PREFIX+field_name); + if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.PREFIX + field_name); } } } @@ -1924,7 +1970,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Build the popup if(!this.selectPopup) { - const select = et2_createWidget("select", { + const select = et2_createWidget("select", { multiple: true, rows: 8, empty_label: this.egw().lang("select columns"), @@ -1937,42 +1983,46 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let autoRefresh; if(!this.options.disable_autorefresh) { - autoRefresh = et2_createWidget("select", { + autoRefresh = et2_createWidget("select", { "empty_label": "Refresh" }, this); autoRefresh.set_id("nm_autorefresh"); autoRefresh.set_select_options({ - // Cause [unknown] problems with mail - 30: "30 seconds", - //60: "1 Minute", - 180: "3 Minutes", - 300: "5 Minutes", - 900: "15 Minutes", - 1800: "30 Minutes" - }); + // Cause [unknown] problems with mail + 30: "30 seconds", + //60: "1 Minute", + 180: "3 Minutes", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }); autoRefresh.set_value(this._get_autorefresh()); autoRefresh.set_statustext(egw.lang("Automatically refresh list")); } - const defaultCheck = et2_createWidget("select", {"empty_label": "Preference"}, this); + const defaultCheck = et2_createWidget("select", {"empty_label": "Preference"}, this); defaultCheck.set_id('nm_col_preference'); defaultCheck.set_select_options({ - 'default': {label: 'Default',title:'Set these columns as the default'}, - 'reset': {label: 'Reset', title:"Reset all user's column preferences"}, - 'force': {label: 'Force', title:'Force column preference so users cannot change it'} + 'default': {label: 'Default', title: 'Set these columns as the default'}, + 'reset': {label: 'Reset', title: "Reset all user's column preferences"}, + 'force': {label: 'Force', title: 'Force column preference so users cannot change it'} }); - defaultCheck.set_value(this.options.settings.columns_forced ? 'force': ''); + defaultCheck.set_value(this.options.settings.columns_forced ? 'force' : ''); - const okButton = et2_createWidget("buttononly", {"background_image": true, image: "check"}, this); + const okButton = et2_createWidget("buttononly", { + "background_image": true, + image: "check" + }, this); okButton.set_label(this.egw().lang("ok")); - okButton.onclick = function() { + okButton.onclick = function() + { // Update visibility const visibility = {}; - for (var i = 0; i < columnMgr.columns.length; i++) + for(var i = 0; i < columnMgr.columns.length; i++) { const col = columnMgr.columns[i]; if(col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED ) + col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { visibility[col.id] = {visible: false}; } @@ -1985,7 +2035,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 var show_letters = true; if(value.indexOf(LETTERS) >= 0) { - value.splice(value.indexOf(LETTERS),1); + value.splice(value.indexOf(LETTERS), 1); } else { @@ -1998,7 +2048,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 for(var i = 0; i < value.length; i++) { // Handle skipped columns - while(value[i] != "col_"+column && column < columnMgr.columns.length) + while(value[i] != "col_" + column && column < columnMgr.columns.length) { column++; } @@ -2007,7 +2057,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 visibility[value[i]].visible = true; } // Custom fields are listed seperately in column list, but are only 1 column - if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { + if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) + { const cf = self.columns[column].widget.options.customfields; const visible = self.columns[column].widget.options.fields; @@ -2029,14 +2080,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 columnMgr.setColumnVisibilitySet(visibility); this.sortedColumnsList = []; - jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i, v) + { const data_id = v.getAttribute('data-value'); const value = select.getValue(); - if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) + if(data_id.match(/^col_/) && value.indexOf(data_id) != -1) { const col_id = data_id.replace('col_', ''); const col_widget = self.columns[col_id].widget; - if (col_widget.customfields) + if(col_widget.customfields) { self.sortedColumnsList.push(col_widget.id); for(let field_name in col_widget.customfields) @@ -2072,9 +2124,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 self.selectPopup = null; }; - const cancelButton = et2_createWidget("buttononly", {"background_image": true, image: "cancel"}, this); + const cancelButton = et2_createWidget("buttononly", { + "background_image": true, + image: "cancel" + }, this); cancelButton.set_label(this.egw().lang("cancel")); - cancelButton.onclick = function() { + cancelButton.onclick = function() + { self.selectPopup.toggle(); self.selectPopup = null; }; @@ -2089,9 +2145,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 }); $select.disableSelection(); - $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + $select.find('li[class^="selcolumn_sortable_"]').each(function(i, v) + { // @ts-ignore - jQuery(v).attr('data-value',(jQuery(v).find('input')[0].value)) + jQuery(v).attr('data-value', (jQuery(v).find('input')[0].value)) }); const $footerWrap = jQuery(document.createElement("div")) .addClass('dialogFooterToolbar') @@ -2124,7 +2181,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 const s_position = this.div.position(); const max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - (2 * this.selectPopup.find('.dialogFooterToolbar').height()); - this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height',max_height); + this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height', max_height); this.selectPopup.css("top", t_position.top) .css("left", s_position.left + this.div.width() - this.selectPopup.width()); } @@ -2153,8 +2210,9 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 { // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields colName = widget.id; - for(let name in widget.options.fields) { - if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.PREFIX+name); + for(let name in widget.options.fields) + { + if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.PREFIX + name); } } if(visibility[colMgr.columns[i].id].visible) @@ -2182,10 +2240,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 const visibility = {}; // Initialize to false - for (var i = 0; i < columnMgr.columns.length; i++) + for(var i = 0; i < columnMgr.columns.length; i++) { const col = columnMgr.columns[i]; - if (col.caption && col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) + if(col.caption && col.visibility != et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { visibility[col.id] = {visible: false}; } @@ -2202,7 +2260,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 visibility[columnMgr.columns[i].id].visible = true; } // Custom fields are listed seperately in column list, but are only 1 column - if(widget && widget.instanceOf(et2_nextmatch_customfields)) { + if(widget && widget.instanceOf(et2_nextmatch_customfields)) + { // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields colName = widget.id; @@ -2234,7 +2293,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.dataview._updateColumns(); // Allow column widgets a chance to resize - this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable); + this.iterateOver(function(widget) + { + widget.resize(); + }, this, et2_IResizeable); } /** @@ -2242,7 +2304,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {boolean} letters_on */ - _set_lettersearch( letters_on) + _set_lettersearch(letters_on) { if(letters_on) { @@ -2253,7 +2315,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.header.lettersearch.hide(); } const lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; - this.egw().set_preference(this.egw().app_name(),lettersearch_preference,letters_on); + this.egw().set_preference(this.egw().app_name(), lettersearch_preference, letters_on); } /** @@ -2261,10 +2323,10 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param time int Refresh period, in seconds */ - _set_autorefresh( time) + _set_autorefresh(time) { // Start / update timer - if (this._autorefresh_timer) + if(this._autorefresh_timer) { window.clearInterval(this._autorefresh_timer); delete this._autorefresh_timer; @@ -2275,36 +2337,42 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 const app = this._get_appname(); if(this._get_autorefresh() != time) { - this.egw().set_preference(app,refresh_preference,time); + this.egw().set_preference(app, refresh_preference, time); } if(time > 0) { this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); // Bind to tab show/hide events, so that we don't bother refreshing in the background - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) + { // Stop window.clearInterval(this._autorefresh_timer); jQuery(e.target).off(e); // If the autorefresh time is up, bind once to trigger a refresh // (if needed) when tab is activated again - this._autorefresh_timer = setTimeout(jQuery.proxy(function() { + this._autorefresh_timer = setTimeout(jQuery.proxy(function() + { // Check in case it was stopped / destroyed since if(!this._autorefresh_timer || !this.getInstanceManager()) return; jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', // Important to use anonymous function instead of just 'this.refresh' because // of the parameters passed - jQuery.proxy(function() {this.refresh(null, 'edit');},this) + jQuery.proxy(function() + { + this.refresh(null, 'edit'); + }, this) ); - },this), time*1000); - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { + }, this), time * 1000); + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) + { // Start normal autorefresh timer again this._set_autorefresh(this._get_autorefresh()); jQuery(e.target).off(e); - },this)); + }, this)); } } @@ -2313,14 +2381,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @return int Refresh period, in secods */ - _get_autorefresh( ) + _get_autorefresh() { if(this.options.disable_autorefresh) { return 0; } const refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - return this.egw().preference(refresh_preference,this._get_appname()); + return this.egw().preference(refresh_preference, this._get_appname()); } /** @@ -2333,7 +2401,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param disabled */ - set_disable_autorefresh( disabled : boolean) + set_disable_autorefresh(disabled : boolean) { this.options.disable_autorefresh = disabled; @@ -2346,12 +2414,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {string} template_name Full template name in the form app.template[.template] */ - set_template( template_name : string) { + set_template(template_name : string) + { const template = et2_createWidget("template", {"id": template_name}, this); - if (this.template) { + if(this.template) + { // Stop early to prevent unneeded processing, and prevent infinite // loops if the server changes the template in get_rows - if (this.template == template_name) { + if(this.template == template_name) + { return; } @@ -2364,9 +2435,11 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Free any children from previous template // They may get left behind because of how detached nodes are processed // We don't use iterateOver because it checks sub-children - for (let i = this._children.length - 1; i >= 0; i--) { + for(let i = this._children.length - 1; i >= 0; i--) + { const _node = this._children[i]; - if (_node != this.header && _node !== template) { + if(_node != this.header && _node !== template) + { this.removeChild(_node); _node.destroy(); } @@ -2374,16 +2447,17 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Clear this setting if it's the same as the template, or // the columns will not be loaded - if (this.template == this.options.settings.columnselection_pref) { + if(this.template == this.options.settings.columnselection_pref) + { this.options.settings.columnselection_pref = template_name; } this.dataview = new et2_dataview(this.innerDiv, this.egw()); } - if (!template) + if(!template) { this.egw().debug("error", "Error while loading definition template for " + - "nextmatch widget.",template_name); + "nextmatch widget.", template_name); return; } @@ -2393,29 +2467,35 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Deferred parse function - template might not be fully loaded - const parse = function (template) { + const parse = function(template) + { // Keep the name of the template, as we'll free up the widget after parsing this.template = template_name; // Fetch the grid element and parse it const definitionGrid = template.getChildren()[0]; - if (definitionGrid && definitionGrid instanceof et2_grid) { + if(definitionGrid && definitionGrid instanceof et2_grid) + { this._parseGrid(definitionGrid); - } else { + } + else + { this.egw().debug("error", "Nextmatch widget expects a grid to be the " + "first child of the defined template."); return; } // Free the template again, but don't remove it - setTimeout(function () { + setTimeout(function() + { template.destroy(); }, 1); // Call the "setNextmatch" function of all registered // INextmatchHeader widgets. This updates this.activeFilters.col_filters according // to what's in the template. - this.iterateOver(function (_node) { + this.iterateOver(function(_node) + { _node.setNextmatch(this); }, this, et2_INextmatchHeader); @@ -2424,12 +2504,14 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // If no data was sent from the server, and num_rows is 0, the nm will be empty. // This triggers a cache check. - if (!this.options.settings.num_rows && this.controller) { + if(!this.options.settings.num_rows && this.controller) + { this.controller.update(); } // Load the default sort order - if (this.options.settings.order && this.options.settings.sort) { + if(this.options.settings.order && this.options.settings.sort) + { this.sortBy(this.options.settings.order, this.options.settings.sort == "ASC", false); } @@ -2444,7 +2526,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Wait until template (& children) are done jQuery.when.apply(null, promise).done( - jQuery.proxy(function() { + jQuery.proxy(function() + { parse.call(this, template); if(!this.dynheight) { @@ -2458,45 +2541,49 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Some accessors to match conventions - set_hide_header( hide : boolean) + set_hide_header(hide : boolean) { (hide ? this.header.div.hide() : this.header.div.show()); } - set_header_left( template : string) + set_header_left(template : string) { - this.header._build_header("left",template); + this.header._build_header("left", template); } - set_header_right( template : string) + + set_header_right(template : string) { - this.header._build_header("right",template); + this.header._build_header("right", template); } - set_header_row( template : string) + + set_header_row(template : string) { - this.header._build_header("row",template); + this.header._build_header("row", template); } - set_no_filter( bool, filter_name) + + set_no_filter(bool, filter_name) { if(typeof filter_name == 'undefined') { filter_name = 'filter'; } - this.options['no_'+filter_name] = bool; + this.options['no_' + filter_name] = bool; let filter = this.header[filter_name]; if(filter) { filter.set_disabled(bool); } - else if (bool) + else if(bool) { filter = this.header._build_select(filter_name, 'select', - this.settings[filter_name], this.settings[filter_name+'_no_lang']); + this.settings[filter_name], this.settings[filter_name + '_no_lang']); } } - set_no_filter2( bool) + + set_no_filter2(bool) { - this.set_no_filter(bool,'filter2'); + this.set_no_filter(bool, 'filter2'); } /** @@ -2507,7 +2594,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {String|number} value */ - set_filter( value) + set_filter(value) { const update = this.update_in_progress; this.update_in_progress = true; @@ -2528,7 +2615,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {String|number} value */ - set_filter2( value) + set_filter2(value) { const update = this.update_in_progress; this.update_in_progress = true; @@ -2564,7 +2651,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {object} actions */ - set_actions( actions : object[]) + set_actions(actions : object[]) { if(actions != this.options.actions && this.controller != null && this.controller._actionManager) { @@ -2608,7 +2695,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * * @param {String|Function} handler */ - set_onfiledrop( handler) + set_onfiledrop(handler) { this.options.onfiledrop = handler; } @@ -2624,7 +2711,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * @param {object} event * @param {object} target */ - handle_drop( event, target) + handle_drop(event, target) { // Check to see if we can handle the link // First, find the UID @@ -2644,7 +2731,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Exectute the custom handler code - if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) + if(this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) { return false; } @@ -2661,7 +2748,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 to_id: split.join('::') }; // Create widget and mangle to our needs - const link = et2_createWidget("link-to", {value: link_value}, this); + const link = et2_createWidget("link-to", {value: link_value}, this); link.loadingFinished(); link.file_upload.set_drop_target(false); @@ -2677,7 +2764,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 .appendTo(row.row.tr); // Bind to link event so we can remove when done - link.div.on('link.et2_link_to', function(e, linked) { + link.div.on('link.et2_link_to', function(e, linked) + { if(!linked) { jQuery("li.success", link.file_upload.progress) @@ -2686,11 +2774,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 else { // Update row - link._parent.refresh(uid,'edit'); + link._parent.refresh(uid, 'edit'); } // Fade out nicely status.delay(linked ? 1 : 2000) - .fadeOut(500, function() { + .fadeOut(500, function() + { link.destroy(); status.remove(); }); @@ -2702,19 +2791,19 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 link.file_upload.set_value(files); } - getDOMNode( _sender?) + getDOMNode(_sender?) { - if (_sender == this || typeof _sender === 'undefined') + if(_sender == this || typeof _sender === 'undefined') { return this.div[0]; } - if (_sender == this.header) + if(_sender == this.header) { return this.header.div[0]; } - for (let i = 0; i < this.columns.length; i++) + for(let i = 0; i < this.columns.length; i++) { - if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) + if(this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) { return this.dataview.getHeaderContainerNode(i); } @@ -2734,13 +2823,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 /** * Get the current 'value' for the nextmatch */ - getValue( ) : ActiveFilters + getValue() : ActiveFilters { const _ids = this.getSelection(); // Translate the internal uids back to server uids const idsArr = _ids.ids; - for (let i = 0; i < idsArr.length; i++) + for(let i = 0; i < idsArr.length; i++) { idsArr[i] = idsArr[i].split("::").pop(); } @@ -2756,10 +2845,21 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } return value; } - resetDirty( ) - {} - isDirty() { return false; } - isValid( ) { return true;} + + resetDirty() + { + } + + isDirty() + { + return false; + } + + isValid() + { + return true; + } + set_value(_value) { this.value = _value; @@ -2772,13 +2872,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * We check for un-loaded rows, and ask the user what they want to do about them. * If they want to print them all, we ask the server and print when they're loaded. */ - beforePrint( ) + beforePrint() { // Add the class, if needed this.div.addClass('print'); // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width',this.div.css('max-width')); + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); this.resize(); // Reset height to auto (after width resize) so there's no restrictions this.dynheight.innerNode.css('height', 'auto'); @@ -2796,7 +2896,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 let pref = this.options.settings.columnselection_pref; if(pref.indexOf('nextmatch') == 0) { - pref = 'nextmatch-'+pref; + pref = 'nextmatch-' + pref; } const app = this.getInstanceManager().app; @@ -2806,34 +2906,34 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 const columns_selected = []; // Get column names - for (let i = 0; i < columnMgr.columns.length; i++) + for(let i = 0; i < columnMgr.columns.length; i++) { const col = columnMgr.columns[i]; const widget = this.columns[i].widget; let colName = this._getColumnName(widget); - if (col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + if(col.caption && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && col.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_DISABLED) { columns[colName] = col.caption; - if (col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); + if(col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); } // Custom fields get listed separately if(widget.instanceOf(et2_nextmatch_customfields)) { - delete(columns[colName]); + delete (columns[colName]); colName = widget.id; - if (col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE && ! + if(col.visibility === et2_dataview_column.ET2_COL_VISIBILITY_VISIBLE && ! jQuery.isEmptyObject((widget).customfields) ) { columns[colName] = col.caption; for(let field_name in (widget).customfields) { - columns[et2_nextmatch_customfields.PREFIX+field_name] = " - "+(widget).customfields[field_name].label; + columns[et2_nextmatch_customfields.PREFIX + field_name] = " - " + (widget).customfields[field_name].label; if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) { - columns_selected.push(et2_nextmatch_customfields.PREFIX+field_name); + columns_selected.push(et2_nextmatch_customfields.PREFIX + field_name); } } } @@ -2841,15 +2941,18 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Preference exists? Set it now - if(this.egw().preference(pref,app)) + if(this.egw().preference(pref, app)) { - this.set_columns(jQuery.extend([],this.egw().preference(pref,app))); + this.set_columns(jQuery.extend([], this.egw().preference(pref, app))); } - const callback = jQuery.proxy(function (button, value) { - if (button === et2_dialog.CANCEL_BUTTON) { + const callback = jQuery.proxy(function(button, value) + { + if(button === et2_dialog.CANCEL_BUTTON) + { // Give dialog a chance to close, or it will be in the print - window.setTimeout(function () { + window.setTimeout(function() + { defer.reject(); }, 0); return; @@ -2869,10 +2972,13 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 style.media = 'print'; // @ts-ignore - if (style.styleSheet) { + if(style.styleSheet) + { // @ts-ignore style.styleSheet.cssText = css; - } else { + } + else + { style.appendChild(document.createTextNode(css)); } @@ -2887,29 +2993,34 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 this.egw().set_preference(app, pref, value.columns); let rows = parseInt(value.row_count); - if (rows > total) { + if(rows > total) + { rows = total; } // If they want the whole thing, style it as all - if (button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) { + if(button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) + { // Add the class, gives more reliable sizing this.div.addClass('print'); // Show it all jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); } // We need more rows - if (button === 'dialog[all]' || rows > loaded_count) { + if(button === 'dialog[all]' || rows > loaded_count) + { let count = 0; let fetchedCount = 0; let cancel = false; const nm = this; const dialog = et2_dialog.show_dialog( // Abort the long task if they canceled the data load - function () { + function() + { count = total; cancel = true; - window.setTimeout(function () { + window.setTimeout(function() + { defer.reject(); }, 0); }, @@ -2920,26 +3031,32 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // dataFetch() is asynchronous, so all these requests just get fired off... // 200 rows chosen arbitrarily to reduce requests. - do { + do + { const ctx = { "self": this.controller, "start": count, "count": Math.min(rows, 200), "lastModification": this.controller._lastModification }; - if (nm.controller.dataStorePrefix) { + if(nm.controller.dataStorePrefix) + { // @ts-ignore ctx.prefix = nm.controller.dataStorePrefix; } - nm.controller.dataFetch({start: count, num_rows: Math.min(rows, 200)}, function (data) { + nm.controller.dataFetch({start: count, num_rows: Math.min(rows, 200)}, function(data) + { // Keep track - if (data && data.order) { + if(data && data.order) + { fetchedCount += data.order.length; } nm.controller._fetchCallback.apply(this, arguments); - if (fetchedCount >= rows) { - if (cancel) { + if(fetchedCount >= rows) + { + if(cancel) + { dialog.destroy(); defer.reject(); return; @@ -2955,7 +3072,8 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); // Grid needs to redraw before it can be printed, so wait - window.setTimeout(jQuery.proxy(function () { + window.setTimeout(jQuery.proxy(function() + { dialog.destroy(); // Should be OK to print now @@ -2966,9 +3084,12 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 }, ctx); count += 200; - } while (count < rows); + } + while(count < rows); nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows + 1)); - } else { + } + else + { // Don't need more rows, limit to requested and finish // Show it all @@ -2982,16 +3103,17 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // No scrollbar in print view jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); // Give dialog a chance to close, or it will be in the print - window.setTimeout(function () { + window.setTimeout(function() + { defer.resolve(); }, 0); } }, this); var value = { content: { - row_count: Math.min(100,total), - columns: this.egw().preference(pref,app) || columns_selected, - orientation: this.egw().preference(pref+'_orientation',app) + row_count: Math.min(100, total), + columns: this.egw().preference(pref, app) || columns_selected, + orientation: this.egw().preference(pref + '_orientation', app) }, sel_options: { columns: columns @@ -3019,7 +3141,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 _create_print_dialog(value, callback) { let base_url = this.getInstanceManager().template_base_url; - if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1); // otherwise we generate a url //api/templates, which is wrong + if(base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice(0, -1); // otherwise we generate a url //api/templates, which is wrong const tab = this.get_tab_info(); // Get title for print dialog from settings or tab, if available const title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); @@ -3037,7 +3159,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 * Try to clean up the mess we made getting ready for printing * in beforePrint() */ - afterPrint( ) + afterPrint() { if(!this.div.hasClass('print')) { @@ -3048,7 +3170,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 delete this.print.orientation_style; // Put scrollbar back - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y',''); + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', ''); // Correct size of grid, and trigger resize to fix it this.controller._grid.setScrollHeight(this.print.old_height); @@ -3062,7 +3184,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } // Restore columns - let pref: string | object | boolean = []; + let pref : string | object | boolean = []; const app = this.getInstanceManager().app; if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { @@ -3071,18 +3193,19 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 else { // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); } if(pref) { if(typeof pref === 'string') pref = (pref).split(','); // @ts-ignore - this.set_columns(pref,app); + this.set_columns(pref, app); } - this.dynheight.outerNode.css('max-width','inherit'); + this.dynheight.outerNode.css('max-width', 'inherit'); this.resize(); } } + et2_register_widget(et2_nextmatch, ["nextmatch"]); /** @@ -3094,7 +3217,7 @@ et2_register_widget(et2_nextmatch, ["nextmatch"]); */ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INextmatchHeader { - static readonly _attributes: any = { + static readonly _attributes : any = { "filter_label": { "name": "Filter label", "type": "string", @@ -3122,32 +3245,32 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext "default": false } }; - headers: {id: string}[] | et2_widget[]; - et2_searchbox: et2_inputWidget; - private favorites: et2_DOMWidget; // Actually favorite + headers : { id : string }[] | et2_widget[]; + et2_searchbox : et2_inputWidget; + private favorites : et2_DOMWidget; // Actually favorite - private nextmatch: et2_nextmatch; - div: JQuery; - private update_in_progress: boolean; + private nextmatch : et2_nextmatch; + div : JQuery; + private update_in_progress : boolean; - header_div: JQuery; - private header_row: JQuery; - private filter_div: JQuery; - private row_div: JQuery; - private fav_span: JQuery; - private toggle_header: JQuery; - lettersearch: JQuery; + header_div : JQuery; + private header_row : JQuery; + private filter_div : JQuery; + private row_div : JQuery; + private fav_span : JQuery; + private toggle_header : JQuery; + lettersearch : JQuery; - private delete_action: JQuery; - private action_header: JQuery; + private delete_action : JQuery; + private action_header : JQuery; - private search_box: JQuery; - private category: any; - private filter: et2_selectbox; - private filter2: et2_selectbox; - private right_div: JQuery; - private count: JQuery; - private count_total: JQuery; + private search_box : JQuery; + private category : any; + private filter : et2_selectbox; + private filter2 : et2_selectbox; + private right_div : JQuery; + private count : JQuery; + private count_total : JQuery; /** * Constructor @@ -3158,7 +3281,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext */ constructor(_parent : et2_nextmatch, _attrs? : WidgetConfig, _child? : object) { - super(_parent, [_parent,_parent.options.settings], ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})); + super(_parent, [_parent, _parent.options.settings], ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})); this.nextmatch = _parent; this.div = jQuery(document.createElement("div")) .addClass("nextmatch_header"); @@ -3168,7 +3291,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext this.update_in_progress = false; } - destroy( ) + destroy() { this.nextmatch = null; @@ -3176,7 +3299,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext this.div = null; } - setNextmatch( nextmatch) + setNextmatch(nextmatch) { const create_once = (this.nextmatch == null); this.nextmatch = nextmatch; @@ -3186,7 +3309,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } // Bind row count - this.nextmatch.dataview.grid.setInvalidateCallback(function () { + this.nextmatch.dataview.grid.setInvalidateCallback(function() + { this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); }, this); } @@ -3196,10 +3320,11 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * * @param {object} actions */ - set_actions( actions : object[]) - {} + set_actions(actions : object[]) + { + } - _createHeader( ) + _createHeader() { let button; @@ -3211,9 +3336,9 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Left & Right (& row) headers this.headers = [ - {id:this.nextmatch.options.header_left}, - {id:this.nextmatch.options.header_right}, - {id:this.nextmatch.options.header_row} + {id: this.nextmatch.options.header_left}, + {id: this.nextmatch.options.header_right}, + {id: this.nextmatch.options.header_row} ]; // The rest of the header @@ -3227,75 +3352,86 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Search this.search_box = jQuery(document.createElement("div")) .addClass('search') - .prependTo(egwIsMobile()?this.nextmatch.getDOMNode():this.row_div) + .prependTo(egwIsMobile() ? this.nextmatch.getDOMNode() : this.row_div) // searchbox widget options const searchbox_options = { id: "search", overlay: (typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined') ? settings.searchbox.overlay : false, - onchange: function () { + onchange: function() + { self.nextmatch.applyFilters({search: this.get_value()}); }, value: settings.search, fix: !egwIsMobile() }; // searchbox widget - this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this); + this.et2_searchbox = et2_createWidget('searchbox', searchbox_options, this); // Set activeFilters to current value this.nextmatch.activeFilters.search = settings.search; this.et2_searchbox.set_value(settings.search); - jQuery(this.et2_searchbox.getInputNode()).attr("aria-label",egw.lang("search")); + jQuery(this.et2_searchbox.getInputNode()).attr("aria-label", egw.lang("search")); /** * Mobile theme specific part for nm header * nm header has very different behaivior for mobile theme and basically * it has its own markup separately from nm header in normal templates. */ - if (egwIsMobile()) + if(egwIsMobile()) { this.search_box.addClass('nm-mob-header'); - jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide'); + jQuery(this.div).css({display: 'inline-block'}).addClass('nm_header_hide'); //indicates appname in header jQuery(document.createElement('div')) - .addClass('nm_appname_header') - .text(egw.lang(egw.app_name())) - .appendTo(this.search_box); + .addClass('nm_appname_header') + .text(egw.lang(egw.app_name())) + .appendTo(this.search_box); this.delete_action = jQuery(document.createElement('div')) - .addClass('nm_delete_action') - .prependTo(this.search_box); + .addClass('nm_delete_action') + .prependTo(this.search_box); // toggle header // add new button this.fav_span = jQuery(document.createElement('div')) - .addClass('nm_favorites_div') - .prependTo(this.search_box); + .addClass('nm_favorites_div') + .prependTo(this.search_box); // toggle header menu this.toggle_header = jQuery(document.createElement('button')) - .addClass('nm_toggle_header') - .click(function(){ - jQuery(self.div).toggleClass('nm_header_hide'); - jQuery(this).toggleClass('nm_toggle_header_on'); - window.setTimeout(function(){self.nextmatch.resize();},800); - }) - .prependTo(this.search_box); + .addClass('nm_toggle_header') + .click(function() + { + jQuery(self.div).toggleClass('nm_header_hide'); + jQuery(this).toggleClass('nm_toggle_header_on'); + window.setTimeout(function() + { + self.nextmatch.resize(); + }, 800); + }) + .prependTo(this.search_box); // Context menu this.action_header = jQuery(document.createElement('button')) - .addClass('nm_action_header') - .hide() - .click (function(e){ - // @ts-ignore - jQuery('tr.selected',self.nextmatch.getDOMNode()).trigger({type:'contextmenu',which:3,originalEvent:e}); - }) - .prependTo(this.search_box); + .addClass('nm_action_header') + .hide() + .click(function(e) + { + // @ts-ignore + jQuery('tr.selected', self.nextmatch.getDOMNode()).trigger({ + type: 'contextmenu', + which: 3, + originalEvent: e + }); + }) + .prependTo(this.search_box); } // Add category - if(!settings.no_cat) { - if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; + if(!settings.no_cat) + { + if(typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; this.category = this._build_select('cat_id', settings.cat_is_select ? - 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { multiple: false, tags: true, class: "select-cat", @@ -3304,19 +3440,21 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } // Filter 1 - if(!settings.no_filter) { + if(!settings.no_filter) + { this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); } // Filter 2 - if(!settings.no_filter2) { + if(!settings.no_filter2) + { this.filter2 = this._build_select('filter2', 'select', settings.filter2, - settings.filter2_no_lang, { - multiple: false, - tags: settings.filter2_tags, - class: "select-cat", - value_class: settings.filter2_class - }); + settings.filter2_no_lang, { + multiple: false, + tags: settings.filter2_tags, + class: "select-cat", + value_class: settings.filter2_class + }); } // Other stuff @@ -3345,14 +3483,20 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext { definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().app_name()); } - let button = et2_createWidget("buttononly", {id: "export", "statustext": "Export", image: "download", "background_image": true}, this); + let button = et2_createWidget("buttononly", { + id: "export", + "statustext": "Export", + image: "download", + "background_image": true + }, this); jQuery(button.getDOMNode()) - .click(this.nextmatch, function(event) { + .click(this.nextmatch, function(event) + { // @ts-ignore - egw_openWindowCentered2( egw.link('/index.php', { - 'menuaction': 'importexport.importexport_export_ui.export_dialog', - 'appname': event.data.egw().getAppName(), - 'definition': definition + egw_openWindowCentered2(egw.link('/index.php', { + 'menuaction': 'importexport.importexport_export_ui.export_dialog', + 'appname': event.data.egw().getAppName(), + 'definition': definition }), '_blank', 850, 440, 'yes'); }); } @@ -3363,8 +3507,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Letter search const current_letter = this.nextmatch.options.settings.searchletter ? - this.nextmatch.options.settings.searchletter : - (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); + this.nextmatch.options.settings.searchletter : + (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); if(this.nextmatch.options.settings.lettersearch || current_letter) { this.lettersearch = jQuery(document.createElement("table")) @@ -3376,7 +3520,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Capitals, A-Z const letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); - for(let i in letters) { + for(let i in letters) + { button = jQuery(document.createElement("td")) .addClass("lettersearch") .appendTo(row) @@ -3391,9 +3536,10 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext .text(egw.lang("all")); if(!current_letter) button.addClass("lettersearch_active"); - this.lettersearch.click(this.nextmatch, function(event) { + this.lettersearch.click(this.nextmatch, function(event) + { // this is the lettersearch table - jQuery("td",this).removeClass("lettersearch_active"); + jQuery("td", this).removeClass("lettersearch_active"); jQuery(event.target).addClass("lettersearch_active"); event.data.applyFilters({searchletter: event.target.id || false}); }); @@ -3402,7 +3548,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } // Apply letter search preference const lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; - if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().app_name())) + if(this.lettersearch && !egw.preference(lettersearch_preference, this.nextmatch.egw().app_name())) { this.lettersearch.hide(); } @@ -3415,7 +3561,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * @param {string} location One of left, right, or row * @param {string} template_name Name of the template to load into the location */ - _build_header( location : "left" | "right" | "row", template_name : string) + _build_header(location : "left" | "right" | "row", template_name : string) { const id = location == "left" ? 0 : (location == "right" ? 1 : 2); const existing = this.headers[id]; @@ -3430,15 +3576,17 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Load the template const self = this; - const header = et2_createWidget("template", {"id": template_name}, this); + const header = et2_createWidget("template", {"id": template_name}, this); this.headers[id] = header; const deferred = []; header.loadingFinished(deferred); // Wait until all child widgets are loaded, then bind - jQuery.when.apply(jQuery,deferred).then(function() { + jQuery.when.apply(jQuery, deferred).then(function() + { // fix order in DOM by reattaching templates in correct position - switch (id) { + switch(id) + { case 0: // header_left: prepend jQuery(header.getDOMNode()).prependTo(self.header_div); break; @@ -3446,7 +3594,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); break; case 2: // header_row: after search - window.setTimeout(function(){ // otherwise we might end up after filters + window.setTimeout(function() + { // otherwise we might end up after filters jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); }, 1); break; @@ -3465,7 +3614,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * @param {string} lang * @param {object} extra */ - _build_select( name: string, type: string, value: string, lang: string|boolean, extra?: object) : et2_selectbox + _build_select(name : string, type : string, value : string, lang : string | boolean, extra? : object) : et2_selectbox { const widget_options = jQuery.extend({ "id": name, @@ -3483,7 +3632,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Check parent sel_options, because those are usually global and don't get passed down if(!options) options = this.nextmatch.getArrayMgr("sel_options").getParentMgr()?.getEntry(name); // Sometimes legacy stuff puts it in here - if(!options) options = mgr.getEntry('rows[sel_options]['+name+']'); + if(!options) options = mgr.getEntry('rows[sel_options][' + name + ']'); // Maybe in a row, and options got stuck in ${row} instead of top level const row_stuck = ['${row}', '{$row}']; @@ -3494,17 +3643,17 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) { - row_id = name.replace(/[0-9]+/,row_stuck[i]); + row_id = name.replace(/[0-9]+/, row_stuck[i]); options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); if(!options) { - row_id = row_stuck[i] + "["+name+"]"; + row_id = row_stuck[i] + "[" + name + "]"; options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); } } if(options) { - this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name); + this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].', row_id, name); } } // Legacy: Add in 'All' option for cat_id, if not provided. @@ -3530,7 +3679,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() select.attributes.select_options.ignore = true; - if (this.nextmatch.options.settings[name+"_onchange"]) + if(this.nextmatch.options.settings[name + "_onchange"]) { // Get the onchange function string let onchange = this.nextmatch.options.settings[name + "_onchange"]; @@ -3538,8 +3687,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Real submits cause all sorts of problems if(onchange.match(/this\.form\.submit/)) { - this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name); - onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;'); + this.egw().debug("warn", "%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.", name); + onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/, 'return true;'); } // Connect it to the onchange event of the input element - may submit @@ -3548,7 +3697,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } else // default request changed rows with new filters, previous this.form.submit() { - input.change(this.nextmatch, function(event) { + input.change(this.nextmatch, function(event) + { const set = {}; set[name] = select.getValue(); event.data.applyFilters(set); @@ -3563,7 +3713,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of * additional fields/settings to add in to the favorite. */ - _setup_favorites( filters) + _setup_favorites(filters) { if(typeof filters == "undefined" || filters === false) { @@ -3580,7 +3730,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext this.favorites = et2_createWidget('favorites', widget_options, this); // Add into header - jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div); + jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile() ? this.search_box.find('.nm_favorites_div').show() : this.right_div); } /** @@ -3591,7 +3741,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * * @param filters Array Key => Value pairs of current filters */ - setFilters( filters) + setFilters(filters) { // Avoid loops cause by change events @@ -3601,7 +3751,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Use an array mgr to hande non-simple IDs const mgr = new et2_arrayMgr(filters); - this.iterateOver(function(child) { + this.iterateOver(function(child) + { // Skip favorites, don't want them in the filter if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return; @@ -3609,7 +3760,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext if(typeof child.set_value != "undefined" && child.id) { value = mgr.getEntry(child.id); - if (value == null) value = ''; + if(value == null) value = ''; /** * Sometimes a filter value is not in current options. This can * happen in a saved favorite, for example, or if server changes @@ -3621,18 +3772,18 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext { let found = typeof child.options.select_options[value] != 'undefined'; // options is array of objects with attribute value&label - if (jQuery.isArray(child.options.select_options)) + if(jQuery.isArray(child.options.select_options)) { - for(let o=0; o < child.options.select_options.length; ++o) + for(let o = 0; o < child.options.select_options.length; ++o) { - if (child.options.select_options[o].value == value) + if(child.options.select_options[o].value == value) { found = true; break; } } } - if (!found) + if(!found) { const old_options = child.options.select_options; // Actual label is not available, obviously, or it would be there @@ -3653,8 +3804,8 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext for(let i = 0; i < indexes.length; i++) { - indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); - if (i < indexes.length-1) + indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); + if(i < indexes.length - 1) { if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {}; target = target[indexes[i]]; @@ -3670,11 +3821,11 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Letter search if(this.nextmatch.options.settings.lettersearch) { - jQuery("td",this.lettersearch).removeClass("lettersearch_active"); - jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']",this.lettersearch).addClass("lettersearch_active"); + jQuery("td", this.lettersearch).removeClass("lettersearch_active"); + jQuery(filters.searchletter ? "td#" + filters.searchletter : "td.lettersearch[id='']", this.lettersearch).addClass("lettersearch_active"); // Set activeFilters to current value - filters.searchletter = jQuery("td.lettersearch_active",this.lettersearch).attr("id") || false; + filters.searchletter = jQuery("td.lettersearch_active", this.lettersearch).attr("id") || false; } // Reset flag @@ -3686,7 +3837,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * * @param {et2_widget} _sender */ - getDOMNode( _sender) + getDOMNode(_sender) { const filters = [this.category, this.filter, this.filter2]; for(let i = 0; i < filters.length; i++) @@ -3716,15 +3867,17 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * * @param {et2_template} sub_header */ - _bindHeaderInput( sub_header) + _bindHeaderInput(sub_header) { const header = this; - const bind_change = function (_widget) { + const bind_change = function(_widget) + { // Previously set change function const widget_change = _widget.change; - let change = function (_node) { + let change = function(_node) + { // Call previously set change function const result = widget_change.call(_widget, _node, header.nextmatch); @@ -3732,29 +3885,36 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext let entry = header.nextmatch.activeFilters; const path = _widget.getArrayMgr('content').explodeKey(_widget.id); let i = 0; - if (path.length > 0) { - for (; i < path.length; i++) { + if(path.length > 0) + { + for(; i < path.length; i++) + { entry = entry[path[i]]; } } // Update filters, if the value is different and we're not already doing so - if ((result || typeof result === 'undefined') && entry != _widget.getValue() && !header.update_in_progress) + if((result || typeof result === 'undefined') && entry != _widget.getValue() && !header.update_in_progress) { // Widget will not have an entry in getValues() because nulls // are not returned, we remove it from activeFilters - if (_widget._oldValue == null) { + if(_widget._oldValue == null) + { const path = _widget.getArrayMgr('content').explodeKey(_widget.id); - if (path.length > 0) { + if(path.length > 0) + { let entry = header.nextmatch.activeFilters; let i = 0; - for (; i < path.length - 1; i++) { + for(; i < path.length - 1; i++) + { entry = entry[path[i]]; } delete entry[path[i]]; } header.nextmatch.applyFilters(header.nextmatch.activeFilters); - } else { + } + else + { // Not null is easy, just get values const value = this.getInstanceManager().getValues(sub_header); header.nextmatch.applyFilters(value[header.nextmatch.id]); @@ -3783,6 +3943,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } } } + et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); /** @@ -3792,7 +3953,7 @@ et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); */ export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmatchHeader { - static readonly _attributes: any = { + static readonly _attributes : any = { "label": { "name": "Caption", "type": "string", @@ -3800,16 +3961,17 @@ export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmat "translate": true } }; - protected labelNode: JQuery; - protected nextmatch: et2_nextmatch; - private label: string; + protected labelNode : JQuery; + protected nextmatch : et2_nextmatch; + private label : string; /** * Constructor * * @memberOf et2_nextmatch_header */ - constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})); @@ -3825,12 +3987,12 @@ export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmat * * @param {et2_nextmatch} _nextmatch */ - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; } - set_label( _value) + set_label(_value) { this.label = _value; @@ -3840,6 +4002,7 @@ export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmat this.labelNode.toggleClass('et2_label_empty', !_value); } } + et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); /** @@ -3851,7 +4014,7 @@ et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); */ export class et2_nextmatch_customfields extends et2_customfields_list implements et2_INextmatchHeader { - static readonly _attributes: any = { + static readonly _attributes : any = { 'customfields': { 'name': 'Custom fields', 'description': 'Auto filled' @@ -3861,27 +4024,28 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements "description": "Auto filled" } }; - private nextmatch: et2_nextmatch; + private nextmatch : et2_nextmatch; /** * Constructor * * @memberOf et2_nextmatch_customfields */ - constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})); // Specifically take the whole column this.table.css("width", "100%"); } - destroy( ) + destroy() { this.nextmatch = null; super.destroy(); } - transformAttributes( _attrs) + transformAttributes(_attrs) { super.transformAttributes(_attrs); @@ -3894,12 +4058,12 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); for(let key in data) { - if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key]; + if(typeof data[key] === 'object' && !_attrs[key]) _attrs[key] = data[key]; } } } - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; this.loadFields(); @@ -3908,7 +4072,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements /** * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio */ - loadFields( ) + loadFields() { if(this.nextmatch == null) { @@ -3955,7 +4119,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements { if(field.values && typeof field.values[''] !== 'undefined') { - delete(field.values['']); + delete (field.values['']); } widget = et2_createWidget( field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", @@ -3967,7 +4131,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements this ); } - else if (apps[field.type]) + else if(apps[field.type]) { widget = et2_createWidget("nextmatch-entryheader", { id: cf_id, @@ -3994,7 +4158,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements { cf.hide(); } - else if (jQuery.isEmptyObject(this.options.fields)) + else if(jQuery.isEmptyObject(this.options.fields)) { // If we're showing it make sure it's set, but only after set_fields[field_name] = true; @@ -4008,7 +4172,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements * * @param {array} _fields */ - set_visible( _fields) + set_visible(_fields) { super.set_visible(_fields); @@ -4017,7 +4181,8 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements if(this.nextmatch) { this.nextmatch.iterateOver( - function(widget) { + function(widget) + { if(widget == self) return; widget.set_visible(_fields); }, this, et2_customfields_list @@ -4030,7 +4195,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements * * If only one custom field, just use that, otherwise use "custom fields" */ - _genColumnCaption( ) + _genColumnCaption() { return egw.lang("Custom fields"); } @@ -4039,7 +4204,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements * Provide own column naming, including only selected columns - only useful * to nextmatch itself, not for sending server-side */ - _getColumnName( ) + _getColumnName() { let name = this.id; const visible = []; @@ -4050,16 +4215,17 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements visible.push(et2_customfields_list.PREFIX + field_name); jQuery(this.rows[field_name]).show(); } - else if (typeof this.rows[field_name] != "undefined") + else if(typeof this.rows[field_name] != "undefined") { jQuery(this.rows[field_name]).hide(); } } - if(visible.length) { - name +="_"+ visible.join("_"); + if(visible.length) + { + name += "_" + visible.join("_"); } - else if (this.rows) + else if(this.rows) { // None hidden means all visible jQuery(this.rows[field_name]).parent().parent().children().show(); @@ -4079,6 +4245,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements return name; } } + et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); /** @@ -4087,7 +4254,7 @@ et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); // @ts-ignore export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et2_INextmatchSortable { - static readonly _attributes: any = { + static readonly _attributes : any = { "sortmode": { "name": "Sort order", "type": "string", @@ -4095,15 +4262,16 @@ export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et "translate": false } }; - public static readonly legacyOptions: ['sortmode']; - private sortmode: string; + public static readonly legacyOptions : ['sortmode']; + private sortmode : string; /** * Constructor * * @memberOf et2_nextmatch_sortheader */ - constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})); this.sortmode = "none"; @@ -4111,9 +4279,9 @@ export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et this.labelNode.addClass("nextmatch_sortheader none"); } - click( _event ) + click(_event) { - if (this.nextmatch && super.click( _event )) + if(this.nextmatch && super.click(_event)) { // Send default sort mode if not sorted, otherwise send undefined to calculate this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); @@ -4141,7 +4309,7 @@ export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et * * @param {string} _mode */ - setSortmode( _mode) + setSortmode(_mode) { // Remove the last sortmode class and add the new one this.labelNode.removeClass(this.sortmode) @@ -4151,6 +4319,7 @@ export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et } } + et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); /** @@ -4158,12 +4327,12 @@ et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); */ export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INextmatchHeader, et2_IResizeable { - private nextmatch: et2_nextmatch; + private nextmatch : et2_nextmatch; /** * Override to add change handler */ - createInputWidget( ) + createInputWidget() { // Make sure there's an option for all if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) @@ -4172,7 +4341,8 @@ export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INe } super.createInputWidget(); - jQuery(this.getInputNode()).change(this, function(event) { + jQuery(this.getInputNode()).change(this, function(event) + { if(typeof event.data.nextmatch == 'undefined') { // Not fully set up yet @@ -4194,7 +4364,7 @@ export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INe * * @param {et2_nextmatch} _nextmatch */ - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; @@ -4209,12 +4379,13 @@ export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INe } // Make sure selectbox is not longer than the column - resize( ) + resize() { - this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + this.input.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); } } + et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); /** @@ -4226,7 +4397,7 @@ export class et2_nextmatch_accountfilterheader extends et2_selectAccount impleme * Override to add change handler * */ - createInputWidget( ) + createInputWidget() { // Make sure there's an option for all if(!this.options.empty_label && !this.options.select_options[""]) @@ -4235,7 +4406,8 @@ export class et2_nextmatch_accountfilterheader extends et2_selectAccount impleme } super.createInputWidget(); - this.input.change(this, function(event) { + this.input.change(this, function(event) + { if(typeof event.data.nextmatch == 'undefined') { // Not fully set up yet @@ -4254,7 +4426,7 @@ export class et2_nextmatch_accountfilterheader extends et2_selectAccount impleme * * @param {et2_nextmatch} _nextmatch */ - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; @@ -4264,8 +4436,9 @@ export class et2_nextmatch_accountfilterheader extends et2_selectAccount impleme this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); } } + // Make sure selectbox is not longer than the column - resize( ) + resize() { var max = jQuery(this.parentNode).innerWidth() - 4; var surroundings = this.getSurroundings()._widgetSurroundings; @@ -4273,10 +4446,11 @@ export class et2_nextmatch_accountfilterheader extends et2_selectAccount impleme { max -= jQuery(surroundings[i]).outerWidth(); } - this.input.css("max-width",max + "px"); + this.input.css("max-width", max + "px"); } } + et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); /** @@ -4288,17 +4462,18 @@ et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter export class et2_nextmatch_taglistheader extends et2_taglist implements et2_INextmatchHeader, et2_IResizeable { static readonly _attributes : any = { - autocomplete_url: { default: ''}, - multiple: { default: 'toggle'}, + autocomplete_url: {default: ''}, + multiple: {default: 'toggle'}, onchange: { // @ts-ignore - default: function(event) { + default: function(event) + { if(typeof this.nextmatch === 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; + { + // Not fully set up yet + return; + } + var col_filter = {}; col_filter[this.id] = this.getValue(); // Set value so it's there for response (otherwise it gets cleared if options are updated) //event.data.set_value(event.data.input.val()); @@ -4306,17 +4481,17 @@ export class et2_nextmatch_taglistheader extends et2_taglist implements et2_INex this.nextmatch.applyFilters({col_filter: col_filter}); } }, - rows: { default: 2}, + rows: {default: 2}, class: {default: 'nm_filterheader_taglist'} }; - private nextmatch: et2_nextmatch; + private nextmatch : et2_nextmatch; /** * Override to add change handler * * @memberOf et2_nextmatch_filterheader */ - createInputWidget( ) + createInputWidget() { // Make sure there's an option for all if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) @@ -4345,7 +4520,7 @@ export class et2_nextmatch_taglistheader extends et2_taglist implements et2_INex * * @param {et2_nextmatch} _nextmatch */ - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; @@ -4360,14 +4535,15 @@ export class et2_nextmatch_taglistheader extends et2_taglist implements et2_INex } // Make sure selectbox is not longer than the column - resize( ) + resize() { - this.div.css("height",''); - this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + this.div.css("height", ''); + this.div.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); super.resize(); } } + et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); /** @@ -4382,7 +4558,7 @@ export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INe * @param {object} event * @param {object} selected */ - onchange( event, selected) + onchange(event, selected) { const col_filter = {}; col_filter[this.id] = this.get_value(); @@ -4394,7 +4570,7 @@ export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INe * cases, parent returns an object. If multiple are selected, or anything other than app and * id, the original parent value is returned. */ - getValue( ) + getValue() { let value = super.getValue(); if(typeof value == "object" && value != null) @@ -4410,7 +4586,7 @@ export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INe // we return full value if(typeof value.id == 'string') { - value = value.app +":"+value.id; + value = value.app + ":" + value.id; } } return value; @@ -4422,7 +4598,7 @@ export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INe * * @param {et2_nextmatch} _nextmatch */ - setNextmatch( _nextmatch) + setNextmatch(_nextmatch) { this.nextmatch = _nextmatch; @@ -4443,6 +4619,7 @@ export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INe // Fire on lost focus, clear filter if user emptied box } } + et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); /** @@ -4450,7 +4627,7 @@ et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); */ export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader { - static readonly _attributes: any = { + static readonly _attributes : any = { "widget_type": { "name": "Actual type", "type": "string", @@ -4465,9 +4642,9 @@ export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader "default": {} } }; - public static readonly legacyOptions: ["widget_type","widget_options"]; + public static readonly legacyOptions : ["widget_type", "widget_options"]; - real_node: et2_selectbox; + real_node : et2_selectbox; /** * Constructor @@ -4496,7 +4673,7 @@ export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader _attrs.type = _attrs.widget_type; } } - jQuery.extend(_attrs.widget_options,{id: this.id}); + jQuery.extend(_attrs.widget_options, {id: this.id}); _attrs.id = ''; super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); @@ -4514,16 +4691,17 @@ export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader } // Just pass the real DOM node through, in case anybody asks - getDOMNode( _sender) + getDOMNode(_sender) { return this.real_node ? this.real_node.getDOMNode(_sender) : null; } // Also need to pass through real children - getChildren( ) + getChildren() { return this.real_node.getChildren() || []; } + setNextmatch(_nextmatch : et2_nextmatch) { if(this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) @@ -4532,4 +4710,5 @@ export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader } } } + et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); diff --git a/api/js/etemplate/et2_widget_grid.ts b/api/js/etemplate/et2_widget_grid.ts index 34b04dd1c5..f3c38f182f 100644 --- a/api/js/etemplate/et2_widget_grid.ts +++ b/api/js/etemplate/et2_widget_grid.ts @@ -519,9 +519,9 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl cell.nm_id = node.getAttribute('id'); } // Apply widget's class to td, for backward compatability - if(node.getAttribute("class")) + if (node.getAttribute("class")) { - cell.class += (cell.class ? " " : "") + node.getAttribute("class"); + cell.class += (cell.class ? " " : "") + this.getArrayMgr("content").expandName(node.getAttribute("class")); } // Create the element diff --git a/api/js/etemplate/et2_widget_image.ts b/api/js/etemplate/et2_widget_image.ts index b5b6890139..20adfae5cd 100644 --- a/api/js/etemplate/et2_widget_image.ts +++ b/api/js/etemplate/et2_widget_image.ts @@ -641,7 +641,7 @@ export class et2_avatar extends et2_image } ).sendRequest(true); } - if (this.options.crop) + if (this.options.crop && !this.options.readonly) { jQuery(this.image).cropper({ aspectRatio: 1/1, diff --git a/api/js/etemplate/et2_widget_link.ts b/api/js/etemplate/et2_widget_link.ts index 96667864c3..e39a17ad16 100644 --- a/api/js/etemplate/et2_widget_link.ts +++ b/api/js/etemplate/et2_widget_link.ts @@ -639,7 +639,6 @@ export class et2_link_entry extends et2_inputWidget protected search: JQuery; protected clear: JQuery; protected link_button: JQuery; - private response: any; private request: any; private last_search: string; processing: boolean = false; @@ -1041,6 +1040,13 @@ export class et2_link_entry extends et2_inputWidget }; } } + // display a search query, not a selected entry + else if (_value !== null && typeof _value === 'object' && typeof _value.query === 'string') + { + this.options.value = { app: _value.app || this.options.only_app, id: null }; + this.search.val(_value.query); + return; + } this._oldValue = this.options.value; if (!_value || _value.length == 0 || _value == null || jQuery.isEmptyObject(_value)) { @@ -1183,18 +1189,29 @@ export class et2_link_entry extends et2_inputWidget return response(this.cache[request.term]); } - // Remember callback - this.response = response; - this.search.addClass("loading"); // Remove specific display and revert to CSS file // show() would use inline, should be inline-block this.clear.css('display', ''); - this.request = egw.json("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", - [this.app_select.val(), '', request.term, request.options], - this._results, - this, true, this - ).sendRequest(); + + this.request = egw.request("EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search", + [this.app_select.val(), '', request.term, request.options]); + + this.request.then((data) => + { + if (this.request) + { + this.request = null; + } + this.search.removeClass("loading"); + let result = []; + for (var id in data) + { + result.push({"value": id, "label": data[id]}); + } + this.cache[this.search.val()] = result; + response(result); + }); } /** @@ -1239,27 +1256,6 @@ export class et2_link_entry extends et2_inputWidget }, event.data)); } - /** - * Server found some results - * - * @param {Array} data - */ - _results(data) - { - if (this.request) - { - this.request = null; - } - this.search.removeClass("loading"); - var result = []; - for (var id in data) - { - result.push({"value": id, "label": data[id]}); - } - this.cache[this.search.val()] = result; - this.response(result); - } - /** * Create a link using the current internal values * diff --git a/api/js/etemplate/et2_widget_timestamper.ts b/api/js/etemplate/et2_widget_timestamper.ts index 1d5a23eda0..f14ac67d1a 100644 --- a/api/js/etemplate/et2_widget_timestamper.ts +++ b/api/js/etemplate/et2_widget_timestamper.ts @@ -19,7 +19,7 @@ import {ClassWithAttributes} from "./et2_core_inheritance"; import {et2_no_init} from "./et2_core_common"; import {egw} from "../jsapi/egw_global"; import {et2_IInput} from "./et2_core_interfaces"; - +import {date} from "./lib/date.js"; /** * Class which implements the "button-timestamper" XET-Tag * diff --git a/api/js/etemplate/et2_widget_video.ts b/api/js/etemplate/et2_widget_video.ts index 1b0d44527e..300052ef2d 100644 --- a/api/js/etemplate/et2_widget_video.ts +++ b/api/js/etemplate/et2_widget_video.ts @@ -96,6 +96,18 @@ export class et2_video extends et2_baseWidget implements et2_IDOMNode "type": "boolean", "default": false, "description": "Defines if the video should be played repeatedly" + }, + "volume": { + "name": "Video volume", + "type": "float", + "default": 0, + "description": "Set video's volume" + }, + "playbackrate": { + "name": "Video playBackRate", + "type": "float", + "default": 1, + "description": "Set video's playBackRate" } }; @@ -264,6 +276,105 @@ export class et2_video extends et2_baseWidget implements et2_IDOMNode } } + /** + * Method to set volume + * @param _value + */ + set_volume(_value: number) + { + let value = _value>100?100:_value; + if (value>= 0) + { + if (this._isYoutube() && this.youtube) + { + this.youtube.setVolume(value); + } + else if(!this._isYoutube()) + { + this.video[0].volume = value/100; + } + } + } + + /** + * get volume + */ + get_volume() + { + if (this._isYoutube() && this.youtube) + { + return this.youtube.getVolume(); + } + else + { + return this.video[0].volume * 100; + } + } + + /** + * method to set playBackRate + * @param _value + */ + set_playBackRate(_value: number) + { + let value = _value>16?16:_value; + if (value>= 0) + { + if (this._isYoutube() && this.youtube) + { + this.youtube.setPlaybackRate(value); + } + else + { + this.video[0].playbackRate = value; + } + } + } + + /** + * get playBackRate + */ + get_playBackRate() + { + if (this._isYoutube() && this.youtube) + { + return this.youtube.getPlaybackRate(); + } + else + { + return this.video[0].playbackRate; + } + } + + set_mute(_value) + { + if (this._isYoutube() && this.youtube) { + if (_value) + { + this.youtube.mute(); + } + else + { + this.youtube.unMute(); + } + } + else + { + this.video[0].muted = _value; + } + } + + get_mute() + { + if (this._isYoutube() && this.youtube) + { + return this.youtube.isMuted(); + } + else + { + return this.video[0].muted; + } + } /** * Set poster attribute in order to specify * an image to be shown while video is loading or before user play it diff --git a/api/js/jsapi/egw_app.ts b/api/js/jsapi/egw_app.ts index b3f0836375..5dc1b549aa 100644 --- a/api/js/jsapi/egw_app.ts +++ b/api/js/jsapi/egw_app.ts @@ -782,18 +782,21 @@ export abstract class EgwApp }) .addClass("ui-helper-clearfix"); - let el = document.getElementById('favorite_sidebox_'+this.appname).getElementsByTagName('ul')[0]; - let sortablejs = Sortable.create(el, { - ghostClass: 'ui-fav-sortable-placeholder', - draggable: 'li:not([data-id$="add"])', - delay: 25, - dataIdAttr:'data-id', - onSort: function(event){ - let favSortedList = sortablejs.toArray(); - self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList); - self._refresh_fav_nm(); - } - }); + let el = document.getElementById('favorite_sidebox_'+this.appname)?.getElementsByTagName('ul')[0]; + if (el && el instanceof HTMLElement) + { + let sortablejs = Sortable.create(el, { + ghostClass: 'ui-fav-sortable-placeholder', + draggable: 'li:not([data-id$="add"])', + delay: 25, + dataIdAttr:'data-id', + onSort: function(event){ + let favSortedList = sortablejs.toArray(); + self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList); + self._refresh_fav_nm(); + } + }); + } // Bind favorite de-select var egw_fw = egw_getFramework(); diff --git a/api/js/jsapi/egw_json.js b/api/js/jsapi/egw_json.js index f273b5b561..3f12ed8cc8 100644 --- a/api/js/jsapi/egw_json.js +++ b/api/js/jsapi/egw_json.js @@ -127,16 +127,16 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) }.bind(this); this.websocket = new WebSocket(url); - this.websocket.onopen = jQuery.proxy(function(e) + this.websocket.onopen = (e) => { check_timer = window.setTimeout(check, check_interval); this.websocket.send(JSON.stringify({ subscribe: tokens, account_id: parseInt(account_id) })); - }, this); + }; - this.websocket.onmessage = jQuery.proxy(function(event) + this.websocket.onmessage = (event) => { reconnect_time = min_reconnect_time; console.log(event); @@ -148,18 +148,18 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) { this.handleResponse({ response: [data]}); } - }, this); + }; - this.websocket.onerror = jQuery.proxy(function(error) + this.websocket.onerror = (error) => { reconnect_time *= 2; if (reconnect_time > max_reconnect_time) reconnect_time = max_reconnect_time; console.log(error); (error||this.handleError({}, error)); - }, this); + }; - this.websocket.onclose = jQuery.proxy(function(event) + this.websocket.onclose = (event) => { if (event.wasClean) { @@ -176,9 +176,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) console.log('[close] Connection died --> reconnect in '+reconnect_time+'ms'); if (check_timer) window.clearTimeout(check_timer); check_timer = null; - window.setTimeout(jQuery.proxy(this.openWebSocket, this, url, tokens, account_id, error, reconnect_time), reconnect_time); + window.setTimeout(() => this.openWebSocket(url, tokens, account_id, error, reconnect_time), reconnect_time); } - }, this); + }; }, /** @@ -189,6 +189,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) * @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error * * @return {Promise|boolean} Promise or for async==="keepalive" boolean is returned + * Promise.abort() allows to abort the pending request */ json_request.prototype.sendRequest = function(async, method, error) { @@ -231,7 +232,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) let promise; if (this.async) { - promise = (this.egw.window?this.egw.window:window).fetch(url, init) + const controller = new AbortController(); + const signal = controller.signal; + promise = (this.egw.window?this.egw.window:window).fetch(url, {...init, ...signal}) .then((response) => { if (!response.ok) { throw response; @@ -242,6 +245,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) .catch((_err) => { (error || this.handleError).call(this, _err) }); + + // offering a simple abort mechanism and compatibility with jQuery.ajax + promise.abort = () => controller.abort(); } else { @@ -496,45 +502,40 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd) * @param {string} _menuaction * @param {any[]} _parameters * - * @return Promise + * @return Promise resolving to data part (not full response, which can contain other parts) + * Promise.abort() allows to abort the pending request */ request: function(_menuaction, _parameters) { - let request = new json_request(_menuaction, _parameters, null, this, true, this, this); - let ajax_promise = request.sendRequest(); - - // This happens first, immediately - let resolvePromise = function(resolve, reject) { - // Bind to ajax response - this is called _after_ any other handling - ajax_promise.always(function(response, status, p) { - if(status !== "success") reject(); - - // The ajax request has completed, get just the data & pass it on - if(response && response.response) + const request = new json_request(_menuaction, _parameters, null, this, true, this, this); + const response = request.sendRequest(); + let promise = response.then(function(response) + { + // The ajax request has completed, get just the data & pass it on + if(response && response.response) + { + for(let value of response.response) { - for(let value of response.response) + if(value.type && value.type === "data" && typeof value.data !== "undefined") { - if(value.type && value.type === "data" && typeof value.data !== "undefined") - { - // Data was packed in response - resolve(value.data); - } - else if (value && typeof value.type === "undefined" && typeof value.data === "undefined") - { - // Just raw data - resolve(value); - } + // Data was packed in response + return value.data; + } + else if (value && typeof value.type === "undefined" && typeof value.data === "undefined") + { + // Just raw data + return value; } } - - // No data? Resolve the promise with nothing - resolve(); - }); - }; - - const myPromise = new Promise(resolvePromise); - - return myPromise; + } + return undefined; + }); + // pass abort method to returned response + if (typeof response.abort === 'function') + { + promise.abort = response.abort; + } + return promise; }, /** diff --git a/api/setup/setup.inc.php b/api/setup/setup.inc.php index b169d6feeb..ab8a48d38b 100644 --- a/api/setup/setup.inc.php +++ b/api/setup/setup.inc.php @@ -11,7 +11,7 @@ /* Basic information about this app */ $setup_info['api']['name'] = 'api'; $setup_info['api']['title'] = 'EGroupware API'; -$setup_info['api']['version'] = '21.1'; +$setup_info['api']['version'] = '21.1.001'; $setup_info['api']['versions']['current_header'] = '1.29'; // maintenance release in sync with changelog in doc/rpm-build/debian.changes $setup_info['api']['versions']['maintenance_release'] = '21.1.20210723'; diff --git a/api/setup/tables_update.inc.php b/api/setup/tables_update.inc.php index 33af6b1778..7d4893e720 100644 --- a/api/setup/tables_update.inc.php +++ b/api/setup/tables_update.inc.php @@ -789,5 +789,18 @@ function api_upgrade20_1_002() */ function api_upgrade20_1_003() { - return $GLOBALS['setup_info']['activesync']['currentver'] = '21.1'; + return $GLOBALS['setup_info']['api']['currentver'] = '21.1'; +} + +/** + * Remove non-email addresses from egw_history_log.share_email + * + * @return string + */ +function api_upgrade21_1() +{ + $GLOBALS['egw_setup']->db->query("UPDATE egw_history_log SET share_email=NULL, history_timestamp=history_timestamp". + " WHERE share_email is NOT NULL AND share_email NOT LIKE '%@%'", __LINE__, __FILE__); + + return $GLOBALS['setup_info']['api']['currentver'] = '21.1.001'; } diff --git a/api/src/Etemplate/Widget.php b/api/src/Etemplate/Widget.php index 48cc30bf7d..b5b02787a3 100644 --- a/api/src/Etemplate/Widget.php +++ b/api/src/Etemplate/Widget.php @@ -923,9 +923,10 @@ class Widget /** * Checks if a widget is readonly: - * - readonly attribute set - * - $readonlys[__ALL__] set and $readonlys[$form_name] !== false - * - $readonlys[$form_name] evaluates to true + * 1. $readonlys set to true for $form_name: + * a) $readonlys[$form_name] is set to true (flat array) + * b) self::get_array($readonlys, $form_name) is set to true (hierarchical) + * 2. ($readonlys[__ALL__] or widget readonly attribute) is true AND NOT $readonlys set to false for $form_name * * @param string $cname ='' * @param string $form_name =null form_name, to not calculate him again @@ -940,13 +941,12 @@ class Widget ); $form_name = self::form_name($cname, $this->id, $expand); } - $readonly = $this->attrs['readonly'] || self::$request->readonlys[$form_name] || - self::get_array(self::$request->readonlys,$form_name) === true || - isset(self::$request->readonlys['__ALL__']) && ( - // Exceptions to all - self::$request->readonlys[$form_name] !== false && - self::get_array(self::$request->readonlys,$form_name) !== false - ); + // readonlys can either be set / used as flat array with complete form-name, hierarchical + $readonlys = self::$request->readonlys[$form_name] ?? self::get_array(self::$request->readonlys,$form_name); + + $readonly = $readonlys === true || + // exception to __ALL__ or readonly="true" attribute by setting $readonlys[$from_name] === false + ($this->attrs['readonly'] || isset(self::$request->readonlys['__ALL__'])) && $readonlys !== false; //error_log(__METHOD__."('$cname') this->id='$this->id' --> form_name='$form_name': attrs[readonly]=".array2string($this->attrs['readonly']).", readonlys['$form_name']=".array2string(self::$request->readonlys[$form_name]).", readonlys[$form_name]=".array2string(self::get_array(self::$request->readonlys,$form_name)).", readonlys['__ALL__']=".array2string(self::$request->readonlys['__ALL__'])." returning ".array2string($readonly)); return $readonly; diff --git a/api/src/Html.php b/api/src/Html.php index 74e9194cc8..4acfa409c6 100644 --- a/api/src/Html.php +++ b/api/src/Html.php @@ -513,6 +513,9 @@ tinymce.init({ language: language_code["'. $GLOBALS['egw_info']['user']['preferences']['common']['lang'].'"], language_url: egw.webserverUrl+"/api/js/tinymce/langs/"+language_code[egw.preference("lang", "common")]+".js", browser_spellcheck: true, + images_upload_url: imageUpload, + paste_data_images: true, + paste_filter_drop: true, contextmenu: false, file_picker_callback: function(_callback, _value, _meta){ var callback = _callback; diff --git a/api/src/Link.php b/api/src/Link.php index cafe986086..46c310e169 100644 --- a/api/src/Link.php +++ b/api/src/Link.php @@ -867,7 +867,7 @@ class Link extends Link\Storage if ($id && is_null($title)) // $app,$id has been deleted ==> unlink all links to it { static $unlinking = array(); - // check if we are already trying to unlink the entry, to avoid an infinit recursion + // check if we are already trying to unlink the entry, to avoid an infinite recursion if (!isset($unlinking[$app]) || !isset($unlinking[$app][$id])) { $unlinking[$app][$id] = true; @@ -1344,10 +1344,16 @@ class Link extends Link\Storage Storage\History::static_add($link['app2'],$link['id2'],$GLOBALS['egw_info']['user']['account_id'],'~file~','', Vfs::basename($url)); } } - if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname)) - { - // try removing the dir, in case it's empty - if (($dir = Vfs::dirname($url))) @Vfs::rmdir($dir); + try { + if (($Ok = !file_exists($url) || Vfs::remove($url,true)) && ((int)$app > 0 || $fname)) + { + // try removing the dir, in case it's empty + if (($dir = Vfs::dirname($url))) @Vfs::rmdir($dir); + } + } + catch (\Exception $e) { + // ignore SQL error caused by only virtual directories with non-integer (hash) fs_id + $Ok = false; } if (!is_null($current_is_root)) { diff --git a/api/src/Storage/History.php b/api/src/Storage/History.php index 180aa9c49e..3e6d778efb 100644 --- a/api/src/Storage/History.php +++ b/api/src/Storage/History.php @@ -21,7 +21,7 @@ use EGroupware\Api; /** * Record history logging service * - * This class need to be instanciated for EACH app, which wishes to use it! + * This class need to be instantiated for EACH app, which wishes to use it! */ class History { @@ -57,12 +57,12 @@ class History * * @param string $appname app name this instance operates on */ - function __construct($appname='',$user=null) + function __construct($appname = '', $user = null) { - $this->appname = $appname ? $appname : $GLOBALS['egw_info']['flags']['currentapp']; - $this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id']; + $this->appname = $appname ?: $GLOBALS['egw_info']['flags']['currentapp']; + $this->user = $user ?: $GLOBALS['egw_info']['user']['account_id']; - if (is_object($GLOBALS['egw_setup']->db)) + if(is_object($GLOBALS['egw_setup']->db)) { $this->db = $GLOBALS['egw_setup']->db; } @@ -82,11 +82,11 @@ class History { $where = array('history_appname' => $this->appname); - if (is_array($record_id) || is_numeric($record_id)) + if(is_array($record_id) || is_numeric($record_id)) { $where['history_record_id'] = $record_id; } - $this->db->delete(self::TABLE,$where,__LINE__,__FILE__); + $this->db->delete(self::TABLE, $where, __LINE__, __FILE__); return $this->db->affected_rows(); } @@ -100,14 +100,14 @@ class History { $where = array( 'history_appname' => $this->appname, - 'history_status' => $status + 'history_status' => $status ); - if (is_array($record_id) || is_numeric($record_id)) + if(is_array($record_id) || is_numeric($record_id)) { $where['history_record_id'] = $record_id; } - $this->db->delete(self::TABLE,$where,__LINE__,__FILE__); + $this->db->delete(self::TABLE, $where, __LINE__, __FILE__); return $this->db->affected_rows(); } @@ -120,16 +120,13 @@ class History * @param string $new_value new value * @param string $old_value old value */ - function add($status,$record_id,$new_value,$old_value) + function add($status, $record_id, $new_value, $old_value) { - if ($new_value != $old_value) + if($new_value != $old_value) { - $share_with = ''; - foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj) - { - $share_with .= $share_obj->get_share_with(); - } - $this->db->insert(self::TABLE,array( + $share_with = static::get_share_with($this->appname, $record_id); + + $this->db->insert(self::TABLE, array( 'history_record_id' => $record_id, 'history_appname' => $this->appname, 'history_owner' => $this->user, @@ -137,9 +134,9 @@ class History 'history_new_value' => $new_value, 'history_old_value' => $old_value, 'history_timestamp' => time(), - 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, + 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, 'share_email' => $share_with, - ),false,__LINE__,__FILE__); + ), false, __LINE__, __FILE__); } } @@ -148,14 +145,11 @@ class History */ public static function static_add($appname, $id, $user, $field_code, $new_value, $old_value = '') { - if ($new_value != $old_value) + if($new_value != $old_value) { - $share_with = ''; - foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj) - { - $share_with .= $share_obj->get_share_with(); - } - $GLOBALS['egw']->db->insert(self::TABLE,array( + $share_with = static::get_share_with($appname, $id); + + $GLOBALS['egw']->db->insert(self::TABLE, array( 'history_record_id' => $id, 'history_appname' => $appname, 'history_owner' => (int)$user, @@ -163,12 +157,38 @@ class History 'history_new_value' => $new_value, 'history_old_value' => $old_value, 'history_timestamp' => time(), - 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, + 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, 'share_email' => $share_with, - ),false,__LINE__,__FILE__); + ), false, __LINE__, __FILE__); } } + /** + * If a record was accessed via a share, we want to record who the entry was shared with, rather than the current + * user. Since multiple shares can be active at once, and they might not be for the current entry, we check to + * see if the given entry was accessed via a share, and which share was used. + * The share's share_with is recorded into the history for some hope of tracking who made the change. + * share_with is a list of email addresses, and may be empty. + * + * @param $appname + * @param $id + * + * @return ?string + */ + static function get_share_with($appname, $id) + { + $share_with = null; + foreach(isset($GLOBALS['egw']->sharing) ? $GLOBALS['egw']->sharing : [] as $token => $share_obj) + { + // Make sure share is of the correct type to access an entry, and it is the correct entry + if($share_obj instanceof Api\Link\Sharing && "$appname::$id" === $share_obj['share_path']) + { + $share_with .= $share_obj->get_share_with(); + } + } + return $share_with; + } + /** * Search history-log * @@ -177,13 +197,16 @@ class History * @param string $sort ='DESC' * @param int $limit =null only return this many entries * @return array of arrays with keys id, record_id, appname, owner (account_id), status, new_value, old_value, - * timestamp (Y-m-d H:i:s in servertime), user_ts (timestamp in user-time) + * timestamp (Y-m-d H:i:s in servertime), user_ts (timestamp in user-time) */ - function search($filter,$order='history_id',$sort='DESC',$limit=null) + function search($filter, $order = 'history_id', $sort = 'DESC', $limit = null) { - if (!is_array($filter)) $filter = is_numeric($filter) ? array('history_record_id' => $filter) : array(); + if(!is_array($filter)) + { + $filter = is_numeric($filter) ? array('history_record_id' => $filter) : array(); + } - if (!$order || !preg_match('/^[a-z0-9_]+$/i',$order) || !preg_match('/^(asc|desc)?$/i',$sort)) + if(!$order || !preg_match('/^[a-z0-9_]+$/i', $order) || !preg_match('/^(asc|desc)?$/i', $sort)) { $orderby = 'ORDER BY history_id DESC'; } @@ -193,23 +216,30 @@ class History } foreach($filter as $col => $value) { - if (!is_numeric($col) && substr($col,0,8) != 'history_') + if(!is_numeric($col) && substr($col, 0, 8) != 'history_') { - $filter['history_'.$col] = $value; + $filter['history_' . $col] = $value; unset($filter[$col]); } } - if (!isset($filter['history_appname'])) $filter['history_appname'] = $this->appname; + if(!isset($filter['history_appname'])) + { + $filter['history_appname'] = $this->appname; + } // do not try to read all history entries of an app - if (!$filter['history_record_id']) return array(); + if(!$filter['history_record_id']) + { + return array(); + } $rows = array(); foreach($this->db->select(self::TABLE, '*', $filter, __LINE__, __FILE__, - isset($limit) ? 0 : false, $orderby, 'phpgwapi', $limit) as $row) + isset($limit) ? 0 : false, $orderby, 'phpgwapi', $limit + ) as $row) { $row['user_ts'] = $this->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; - $rows[] = Api\Db::strip_array_keys($row,'history_'); + $rows[] = Api\Db::strip_array_keys($row, 'history_'); } return $rows; } @@ -227,54 +257,58 @@ class History $rows = array(); $filter['history_appname'] = $query['appname']; $filter['history_record_id'] = $query['record_id']; - if(is_array($query['colfilter'])) { - foreach($query['colfilter'] as $column => $value) { + if(is_array($query['colfilter'])) + { + foreach($query['colfilter'] as $column => $value) + { $filter[$column] = $value; } } // filter out private (or no longer defined) custom fields - if ($filter['history_appname']) + if($filter['history_appname']) { $to_or[] = "history_status NOT LIKE '#%'"; // explicitly allow "##" used to store iCal/vCard X-attributes - if (in_array($filter['history_appname'], array('calendar','infolog','addressbook'))) + if(in_array($filter['history_appname'], array('calendar', 'infolog', 'addressbook'))) { $to_or[] = "history_status LIKE '##%'"; } - if (($cfs = Customfields::get($filter['history_appname']))) + if(($cfs = Customfields::get($filter['history_appname']))) { - $to_or[] = 'history_status IN ('.implode(',', array_map(function($str) - { - return $GLOBALS['egw']->db->quote('#'.$str); - }, array_keys($cfs))).')'; + $to_or[] = 'history_status IN (' . implode(',', array_map(function ($str) + { + return $GLOBALS['egw']->db->quote('#' . $str); + }, array_keys($cfs)) + ) . ')'; } - $filter[] = '('.implode(' OR ', $to_or).')'; + $filter[] = '(' . implode(' OR ', $to_or) . ')'; } $_query = array(array( - 'table' => self::TABLE, - 'cols' => array( - 'history_id', - 'history_record_id', - 'history_appname', - 'history_owner', - 'history_status', - 'history_new_value', - 'history_timestamp', - 'history_old_value', - 'share_email' - ), - 'where' => $filter, - )); + 'table' => self::TABLE, + 'cols' => array( + 'history_id', + 'history_record_id', + 'history_appname', + 'history_owner', + 'history_status', + 'history_new_value', + 'history_timestamp', + 'history_old_value', + 'share_email' + ), + 'where' => $filter, + )); // Add in files, if possible if($GLOBALS['egw_info']['user']['apps']['filemanager'] && ($sqlfs_sw = new Api\Vfs\Sqlfs\StreamWrapper()) && - ($file = $sqlfs_sw->url_stat("/apps/{$query['appname']}/{$query['record_id']}",STREAM_URL_STAT_LINK))) + ($file = $sqlfs_sw->url_stat("/apps/{$query['appname']}/{$query['record_id']}", STREAM_URL_STAT_LINK))) { $_query[] = array( 'table' => Api\Vfs\Sqlfs\StreamWrapper::TABLE, - 'cols' =>array('fs_id', 'fs_dir', "'filemanager'",'COALESCE(fs_modifier,fs_creator)',"'~file~'",'fs_name','fs_modified', 'fs_mime', '"" AS share_email'), + 'cols' => array('fs_id', 'fs_dir', "'filemanager'", 'COALESCE(fs_modifier,fs_creator)', "'~file~'", + 'fs_name', 'fs_modified', 'fs_mime', '"" AS share_email'), 'where' => array('fs_dir' => $file['ino']) ); } @@ -290,20 +324,21 @@ class History $row['user_ts'] = $GLOBALS['egw']->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; // Explode multi-part values - foreach(array('history_new_value','history_old_value') as $field) + foreach(array('history_new_value', 'history_old_value') as $field) { - if(strpos($row[$field],Tracking::ONE2N_SEPERATOR) !== false) + if(strpos($row[$field], Tracking::ONE2N_SEPERATOR) !== false) { - $row[$field] = explode(Tracking::ONE2N_SEPERATOR,$row[$field]); + $row[$field] = explode(Tracking::ONE2N_SEPERATOR, $row[$field]); } } - if ($row['history_old_value'] !== Tracking::DIFF_MARKER && ( - static::needs_diff($row['history_status'], $row['history_old_value']) || - static::needs_diff($row['history_status'], $row['history_old_value']) - )) + if($row['history_old_value'] !== Tracking::DIFF_MARKER && ( + static::needs_diff($row['history_status'], $row['history_old_value']) || + static::needs_diff($row['history_status'], $row['history_old_value']) + )) { // Larger text stored with full old / new value - calculate diff and just send that - $diff = new \Horde_Text_Diff('auto', array(explode("\n",$row['history_old_value']), explode("\n",$row['history_new_value']))); + $diff = new \Horde_Text_Diff('auto', array(explode("\n", $row['history_old_value']), + explode("\n", $row['history_new_value']))); $renderer = new \Horde_Text_Diff_Renderer_Unified(); $row['history_new_value'] = $renderer->render($diff); $row['history_old_value'] = Tracking::DIFF_MARKER; @@ -330,18 +365,26 @@ class History $rows[$new_version]['old_value'] = $row['history_new_value']; } } - $rows[] = Api\Db::strip_array_keys($row,'history_'); + + // TODO: This is just here to hide bad values before we clean them with an update. If you're here, remove this IF block + // Clear invalid share_email values + if($row['share_email'] && stripos($row['share_email'], '@') === false) + { + $row['share_email'] = ''; + } + + $rows[] = Api\Db::strip_array_keys($row, 'history_'); } - $total = $GLOBALS['egw']->db->union($_query,__LINE__,__FILE__)->NumRows(); + $total = $GLOBALS['egw']->db->union($_query, __LINE__, __FILE__)->NumRows(); // allow to hook into get_rows of other apps Api\Hooks::process(array( - 'hook_location' => 'etemplate2_history_get_rows', - 'get_rows' => __METHOD__, - 'value' => &$query, - 'rows' => &$rows, - 'total' => &$total, - ), array(), true); // true = no permission check + 'hook_location' => 'etemplate2_history_get_rows', + 'get_rows' => __METHOD__, + 'value' => &$query, + 'rows' => &$rows, + 'total' => &$total, + ), array(), true); // true = no permission check return $total; } @@ -364,9 +407,9 @@ class History { return false; } - return $name == 'note' || // Addressbook - strpos($name, 'description') !== false || // Calendar, Records, Timesheet, ProjectManager, Resources - $name == 'De' || // Tracker, InfoLog + return $name == 'note' || // Addressbook + strpos($name, 'description') !== false || // Calendar, Records, Timesheet, ProjectManager, Resources + $name == 'De' || // Tracker, InfoLog ($value && (strlen($value) > 200 || strstr($value, "\n") !== FALSE)); } } diff --git a/api/src/Vfs/Base.php b/api/src/Vfs/Base.php index 5765ee6bad..24895b0c77 100644 --- a/api/src/Vfs/Base.php +++ b/api/src/Vfs/Base.php @@ -63,7 +63,7 @@ class Base * @var array */ protected static $fstab = array( - '/' => 'sqlfs://$host/', + '/' => 'sqlfs://$host/', '/apps' => 'links://$host/apps', ); @@ -75,73 +75,88 @@ class Base * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/ * @param string $path =null path to mount the filesystem in the vfs, eg. / * @param boolean $check_url =null check if url is an existing directory, before mounting it - * default null only checks if url does not contain a $ as used in $user or $pass + * default null only checks if url does not contain a $ as used in $user or $pass * @param boolean|int $persistent_mount =true create a persitent mount, or only a temprary for current request, - * or integer account_id to mount persistent for a given user or group + * or integer account_id to mount persistent for a given user or group * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount * @return array|boolean array with fstab, if called without parameter or true on successful mount */ - static function mount($url=null, $path=null, $check_url=null, $persistent_mount=true, $clear_fstab=false) + static function mount($url = null, $path = null, $check_url = null, $persistent_mount = true, $clear_fstab = false) { - if (is_null($check_url)) $check_url = strpos($url,'$') === false; + if(is_null($check_url)) + { + $check_url = strpos($url, '$') === false; + } - if (!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup + if(!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup { $api_config = Config::read('phpgwapi'); - if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) + if(isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) { self::$fstab = $api_config['vfs_fstab']; } else { self::$fstab = array( - '/' => 'sqlfs://$host/', + '/' => 'sqlfs://$host/', '/apps' => 'links://$host/apps', ); } unset($api_config); } - if (is_null($url) || is_null($path)) + if(is_null($url) || is_null($path)) { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') returns ' . array2string(self::$fstab)); + } return self::$fstab; } - if (!Vfs::$is_root) + if(!Vfs::$is_root) { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') permission denied, you are NOT root!'); + } + return false; // only root can mount } - if ($clear_fstab) + if($clear_fstab) { self::$fstab = array(); } - if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url) + if(isset(self::$fstab[$path]) && self::$fstab[$path] === $url) { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); - return true; // already mounted + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') already mounted.'); + } + return true; // already mounted } - self::load_wrapper(Vfs::parse_url($url,PHP_URL_SCHEME)); + self::load_wrapper(Vfs::parse_url($url, PHP_URL_SCHEME)); - if ($check_url && (!file_exists($url) || opendir($url) === false)) + if($check_url && (!file_exists($url) || opendir($url) === false)) { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); - return false; // url does not exist + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') url does NOT exist!'); + } + return false; // url does not exist } self::$fstab[$path] = $url; - uksort(self::$fstab, function($a, $b) + uksort(self::$fstab, function ($a, $b) { return strlen($a) - strlen($b); }); - if ($persistent_mount) + if($persistent_mount) { - if ($persistent_mount === true) + if($persistent_mount === true) { - Config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); + Config::save_value('vfs_fstab', self::$fstab, 'phpgwapi'); $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited + if(method_exists($GLOBALS['egw'], 'invalidate_session_cache')) // egw object in setup is limited { $GLOBALS['egw']->invalidate_session_cache(); } @@ -154,10 +169,13 @@ class Base $prefs->save_repository(); // also save for current session $GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'][$path] = - $_SESSION[Api\Session::EGW_INFO_CACHE]['user']['preferences']['common']['vfs_fstab'][$path] = $url; + $_SESSION[Api\Session::EGW_INFO_CACHE]['user']['preferences']['common']['vfs_fstab'][$path] = $url; } } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . '(' . array2string($url) . ',' . array2string($path) . ') returns true (successful new mount).'); + } return true; } @@ -168,18 +186,24 @@ class Base */ static function umount($path) { - if (!Vfs::$is_root) + if(!Vfs::$is_root) { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . '(' . array2string($path) . ',' . array2string($path) . ') permission denied, you are NOT root!'); + } + return false; // only root can mount } - if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false) + if(!isset(self::$fstab[$path]) && ($path = array_search($path, self::$fstab)) === false) { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); - return false; // $path not mounted + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . '(' . array2string($path) . ') NOT mounted!'); + } + return false; // $path not mounted } unset(self::$fstab[$path], $GLOBALS['egw_info']['server']['vfs_fstab'][$path]); - Config::save_value('vfs_fstab', $GLOBALS['egw_info']['server']['vfs_fstab'],'phpgwapi'); + Config::save_value('vfs_fstab', $GLOBALS['egw_info']['server']['vfs_fstab'], 'phpgwapi'); unset($GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'][$path]); unset($_SESSION[Api\Session::EGW_INFO_CACHE]['server']['vfs_fstab'][$path]); @@ -190,11 +214,14 @@ class Base $prefs->save_repository(); // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited + if(method_exists($GLOBALS['egw'], 'invalidate_session_cache')) // egw object in setup is limited { $GLOBALS['egw']->invalidate_session_cache(); } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . '(' . array2string($path) . ') returns true (successful unmount).'); + } return true; } @@ -204,12 +231,12 @@ class Base * @param string $fullurl full url returned by resolve_url * @return string|NULL mount url or null if not found */ - static function mount_url($fullurl, &$mounted=null) + static function mount_url($fullurl, &$mounted = null) { foreach(array_reverse(self::$fstab) as $mounted => $url) { - list($url_no_query) = explode('?',$url); - if (substr($fullurl,0,1+strlen($url_no_query)) === $url_no_query.'/') + list($url_no_query) = explode('?', $url); + if(substr($fullurl, 0, 1 + strlen($url_no_query)) === $url_no_query . '/') { return $url; } @@ -237,71 +264,99 @@ class Base * @param ?string &$mounted =null on return mount-point of resolved url, IF $_path is a path or vfs-url, other urls return NULL! * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry */ - static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false, &$mounted=null) + static function resolve_url($_path, $do_symlink = true, $use_symlinkcache = true, $replace_user_pass_host = true, $fix_url_query = false, &$mounted = null) { $path = self::get_path($_path); // we do some caching here - if (isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host) + if(isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host) { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '".self::$resolve_url_cache[$path]."' (from cache)"); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . "('$path') = '" . self::$resolve_url_cache[$path] . "' (from cache)"); + } $mounted = self::$resolve_url_cache[$path]['mounted']; return self::$resolve_url_cache[$path]['url']; } // check if we can already resolve path (or a part of it) with a known symlinks - if ($use_symlinkcache) + if($use_symlinkcache) { - $path = self::symlinkCache_resolve($path,$do_symlink); + $path = self::symlinkCache_resolve($path, $do_symlink); } // setting default user, passwd and domain, if it's not contained int the url $defaults = array( 'user' => $GLOBALS['egw_info']['user']['account_lid'], 'pass' => urlencode($GLOBALS['egw_info']['user']['passwd']), 'host' => $GLOBALS['egw_info']['user']['domain'], - 'home' => str_replace(array('\\\\','\\'),array('','/'),$GLOBALS['egw_info']['user']['homedirectory']), + 'home' => str_replace(array('\\\\', '\\'), array('', '/'), $GLOBALS['egw_info']['user']['homedirectory']), ); - $parts = array_merge(Vfs::parse_url($_path),Vfs::parse_url($path),$defaults); - if (!$parts['host']) $parts['host'] = 'default'; // otherwise we get an invalid url (scheme:///path/to/something)! - - if (!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME) + $parts = array_merge(Vfs::parse_url($_path), Vfs::parse_url($path), $defaults); + if(!$parts['host']) { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$path' (path is already an url)"); - return $path; // path is already a non-vfs url --> nothing to do + // otherwise we get an invalid url (scheme:///path/to/something)! + $parts['host'] = 'default'; + } + + if(!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME) + { + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . "('$path') = '$path' (path is already an url)"); + } + return $path; // path is already a non-vfs url --> nothing to do + } + if(empty($parts['path'])) + { + $parts['path'] = '/'; } - if (empty($parts['path'])) $parts['path'] = '/'; foreach(array_reverse(self::$fstab) as $mounted => $url) { - if ($mounted == '/' || $mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) + if($mounted == '/' || $mounted == $parts['path'] || $mounted . '/' == substr($parts['path'], 0, strlen($mounted) + 1)) { - $scheme = Vfs::parse_url($url,PHP_URL_SCHEME); - if (is_null(self::$wrappers) || !in_array($scheme,self::$wrappers)) + $scheme = Vfs::parse_url($url, PHP_URL_SCHEME); + if(is_null(self::$wrappers) || !in_array($scheme, self::$wrappers)) { self::load_wrapper($scheme); } - if (($relative = substr($parts['path'],strlen($mounted)))) + if(($relative = substr($parts['path'], strlen($mounted)))) { $url = Vfs::concat($url, $relative); } // if url contains url parameter, eg. from filesystem streamwrapper, we need to append relative path here too $matches = null; - if ($fix_url_query && preg_match('|([?&]url=)([^&]+)|', $url, $matches)) + if($fix_url_query && preg_match('|([?&]url=)([^&]+)|', $url, $matches)) { - $url = str_replace($matches[0], $matches[1].Vfs::concat($matches[2], substr($parts['path'],strlen($mounted))), $url); + $url = str_replace($matches[0], $matches[1] . Vfs::concat($matches[2], substr($parts['path'], strlen($mounted))), $url); } - if ($replace_user_pass_host) + if($replace_user_pass_host) { - $url = str_replace(array('$user','$pass','$host','$home'),array($parts['user'],$parts['pass'],$parts['host'],$parts['home']),$url); + $url = str_replace(array('$user', + '$pass', + '$host', + '$home'), array($parts['user'], + $parts['pass'], + $parts['host'], + $parts['home']), $url); } - if ($parts['query']) $url .= '?'.$parts['query']; - if ($parts['fragment']) $url .= '#'.$parts['fragment']; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$url'"); - - if (($class = self::scheme2class($scheme)) && is_callable([$class, 'replace'])) + if($parts['query']) { - if (!($replace = call_user_func([$class, 'replace'], $url))) + $url .= '?' . $parts['query']; + } + if($parts['fragment']) + { + $url .= '#' . $parts['fragment']; + } + + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . "('$path') = '$url'"); + } + + if(($class = self::scheme2class($scheme)) && is_callable([$class, 'replace'])) + { + if(!($replace = call_user_func([$class, 'replace'], $url))) { return false; } @@ -310,16 +365,17 @@ class Base // Make sure we don't cache anything with a link anywhere in the url, since it fails with eg: /apps/InfoLog/Open$/2021$. // is_link() is not always right here - $is_link = is_link($url) || (self::symlinkCache_resolve(Vfs::parse_url($url,PHP_URL_PATH)) !== Vfs::parse_url($url,PHP_URL_PATH)); + $is_link = is_link($url) || (self::symlinkCache_resolve(Vfs::parse_url($url, PHP_URL_PATH)) !== Vfs::parse_url($url, PHP_URL_PATH)); if($is_link && $do_symlink) { $old_url = $url; $_url = self::symlinkCache_resolve($url); - $url = @readlink($url) ?: (Vfs::parse_url($_url,PHP_URL_PATH) != $parts['path'] ? - str_replace([$parts['path'],Vfs::parse_url($old_url,PHP_URL_SCHEME)],[$_url,Vfs::parse_url(Vfs::resolve_url($_url),PHP_URL_SCHEME)],$url) : null) ?:$url; + $url = @readlink($url) ?: (Vfs::parse_url($_url, PHP_URL_PATH) != $parts['path'] ? + str_replace([$parts['path'], Vfs::parse_url($old_url, PHP_URL_SCHEME)], [$_url, + Vfs::parse_url(Vfs::resolve_url($_url), PHP_URL_SCHEME)], $url) : null) ?: $url; $is_link = $old_url == $url; } - if ($replace_user_pass_host && !$is_link) + if($replace_user_pass_host && !$is_link) { self::$resolve_url_cache[$path] = ['url' => $url, 'mounted' => $mounted]; } @@ -327,8 +383,11 @@ class Base return $url; } } - if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path') can't resolve path!\n"); - trigger_error(__METHOD__."($path) can't resolve path!\n",E_USER_WARNING); + if(self::LOG_LEVEL > 0) + { + error_log(__METHOD__ . "('$path') can't resolve path!\n"); + } + trigger_error(__METHOD__ . "($path) can't resolve path!\n", E_USER_WARNING); return false; } @@ -345,22 +404,37 @@ class Base * @param string $_path vfs path * @param string $target target path */ - static protected function symlinkCache_add($_path,$target) + static protected function symlinkCache_add($_path, $target) { $path = self::get_path($_path); - if (isset(self::$symlink_cache[$path])) return; // nothing to do - - if ($target[0] != '/') $target = Vfs::parse_url($target,PHP_URL_PATH); + if(isset(self::$symlink_cache[$path])) + { + // nothing to do + return; + } + if($target[0] != '/') + { + $query = Vfs::parse_url($target, PHP_URL_QUERY); + $target = Vfs::parse_url($target, PHP_URL_PATH); + if($query) + { + // Don't cache without query, some StreamWrappers need those parameters + $target = "?$query"; + } + } self::$symlink_cache[$path] = $target; // sort longest path first - uksort(self::$symlink_cache, function($b, $a) + uksort(self::$symlink_cache, function ($b, $a) { return strlen($a) - strlen($b); }); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$target) cache now ".array2string(self::$symlink_cache)); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . "($path,$target) cache now " . array2string(self::$symlink_cache)); + } } /** @@ -373,7 +447,10 @@ class Base $path = self::get_path($_path); unset(self::$symlink_cache[$path]); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path) cache now ".array2string(self::$symlink_cache)); + if(self::LOG_LEVEL > 1) + { + error_log(__METHOD__ . "($path) cache now " . array2string(self::$symlink_cache)); + } } /** @@ -385,7 +462,7 @@ class Base * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) * @return string target or path, if path not found */ - public static function symlinkCache_resolve($_path, $do_symlink=true) + public static function symlinkCache_resolve($_path, $do_symlink = true) { // remove vfs scheme, but no other schemes (eg. filesystem!) $path = self::get_path($_path); @@ -396,18 +473,24 @@ class Base { if (($strlen_p = strlen($p)) > $strlen_path) continue; // $path can NOT start with $p - if ($path == $p) + if($path == $p) { - if ($do_symlink) $target = $t; + if($do_symlink) + { + $target = $t; + } break; } - elseif (substr($path,0,$strlen_p+1) == $p.'/') + elseif(substr($path, 0, $strlen_p + 1) == $p . '/') { - $target = $t . substr($path,$strlen_p); + $target = $t . substr($path, $strlen_p); break; } } - if (self::LOG_LEVEL > 1 && isset($target)) error_log(__METHOD__."($path) = $target"); + if(self::LOG_LEVEL > 1 && isset($target)) + { + error_log(__METHOD__ . "($path) = $target"); + } return isset($target) ? $target : $path; } @@ -429,7 +512,7 @@ class Base */ static function load_wrapper($scheme) { - if (!in_array($scheme,self::get_wrappers())) + if(!in_array($scheme, self::get_wrappers())) { switch($scheme) { @@ -440,16 +523,16 @@ class Base self::$wrappers[] = 'webdavs'; break; case '': - break; // default file, always loaded + break; // default file, always loaded default: // check if scheme is buildin in php or one of our own stream wrappers - if (in_array($scheme,stream_get_wrappers()) || class_exists(self::scheme2class($scheme))) + if(in_array($scheme, stream_get_wrappers()) || class_exists(self::scheme2class($scheme))) { self::$wrappers[] = $scheme; } else { - trigger_error("Can't load stream-wrapper for scheme '$scheme'!",E_USER_WARNING); + trigger_error("Can't load stream-wrapper for scheme '$scheme'!", E_USER_WARNING); return false; } } @@ -464,7 +547,7 @@ class Base */ static function get_wrappers() { - if (is_null(self::$wrappers)) + if(is_null(self::$wrappers)) { self::$wrappers = stream_get_wrappers(); } @@ -482,19 +565,23 @@ class Base */ static function scheme2class($scheme) { - if ($scheme === self::SCHEME) + if($scheme === self::SCHEME) { return __CLASS__; } list($app, $app_scheme) = explode('.', $scheme); foreach(array( - empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\'.ucfirst($scheme).'\\StreamWrapper' : // streamwrapper in Api\Vfs - 'EGroupware\\'.ucfirst($app).'\\Vfs\\'.ucfirst($app_scheme).'\\StreamWrapper', // streamwrapper in $app\Vfs - str_replace('.','_',$scheme).'_stream_wrapper', // old (flat) name + empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\' . ucfirst($scheme) . '\\StreamWrapper' : // streamwrapper in Api\Vfs + 'EGroupware\\' . ucfirst($app) . '\\Vfs\\' . ucfirst($app_scheme) . '\\StreamWrapper', + // streamwrapper in $app\Vfs + str_replace('.', '_', $scheme) . '_stream_wrapper', // old (flat) name ) as $class) { //error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class))); - if (class_exists($class)) return $class; + if(class_exists($class)) + { + return $class; + } } } @@ -505,18 +592,18 @@ class Base * @param string $only_remove_scheme =self::SCHEME if given only that scheme get's removed * @return string path without training slash */ - static protected function get_path($path,$only_remove_scheme=self::SCHEME) + static protected function get_path($path, $only_remove_scheme = self::SCHEME) { - if ($path[0] != '/' && (!$only_remove_scheme || Vfs::parse_url($path, PHP_URL_SCHEME) == $only_remove_scheme)) + if($path[0] != '/' && (!$only_remove_scheme || Vfs::parse_url($path, PHP_URL_SCHEME) == $only_remove_scheme)) { $path = Vfs::parse_url($path, PHP_URL_PATH); } // remove trailing slashes eg. added by WebDAV, but do NOT remove / from "sqlfs://default/"! - if ($path != '/') + if($path != '/') { - while (mb_substr($path, -1) == '/' && $path != '/' && ($path[0] == '/' || Vfs::parse_url($path, PHP_URL_PATH) != '/')) + while(mb_substr($path, -1) == '/' && $path != '/' && ($path[0] == '/' || Vfs::parse_url($path, PHP_URL_PATH) != '/')) { - $path = mb_substr($path,0,-1); + $path = mb_substr($path, 0, -1); } } return $path; @@ -532,7 +619,7 @@ class Base { static $cache = array(); $ret =& $cache[$url]; - if (!isset($ret)) + if(!isset($ret)) { $matches = null; $ret = preg_match('/\?(.*&)?ro=([^&]+)/', $url, $matches) && $matches[2]; @@ -548,52 +635,58 @@ class Base * @param string $name * @param array $params first param has to be the path, otherwise we can not determine the correct wrapper * @param boolean|"null" $fail_silent =false should only false be returned if function is not supported by the backend, - * or should an E_USER_WARNING error be triggered (default), or "null": return NULL + * or should an E_USER_WARNING error be triggered (default), or "null": return NULL * @param int $path_param_key =0 key in params containing the path, default 0 * @param boolean $instanciate =false true: instanciate the class to call method $name, false: static call * @return mixed return value of backend or false if function does not exist on backend */ - protected static function _call_on_backend($name, array $params, $fail_silent=false, $path_param_key=0, $instanciate=false) + protected static function _call_on_backend($name, array $params, $fail_silent = false, $path_param_key = 0, $instanciate = false) { $pathes = $params[$path_param_key]; $scheme2urls = array(); foreach(is_array($pathes) ? $pathes : array($pathes) as $path) { - if (!($url = Vfs::resolve_url_symlinks($path,false,false))) + if(!($url = Vfs::resolve_url_symlinks($path, false, false))) { return false; } - $k=(string)Vfs::parse_url($url,PHP_URL_SCHEME); - if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array(); + $k = (string)Vfs::parse_url($url, PHP_URL_SCHEME); + if(!(is_array($scheme2urls[$k]))) + { + $scheme2urls[$k] = array(); + } $scheme2urls[$k][$path] = $url; } $ret = array(); foreach($scheme2urls as $scheme => $urls) { - if ($scheme) + if($scheme) { - if (!class_exists($class = Vfs\StreamWrapper::scheme2class($scheme)) || !method_exists($class,$name)) + if(!class_exists($class = Vfs\StreamWrapper::scheme2class($scheme)) || !method_exists($class, $name)) { - if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING); + if(!$fail_silent) + { + trigger_error("Can't $name for scheme $scheme!\n", E_USER_WARNING); + } return $fail_silent === 'null' ? null : false; } $callback = [$instanciate ? new $class($url) : $class, $name]; - if (!is_array($pathes)) + if(!is_array($pathes)) { $params[$path_param_key] = $url; return call_user_func_array($callback, $params); } $params[$path_param_key] = $urls; - if (!is_array($r = call_user_func_array($callback, $params))) + if(!is_array($r = call_user_func_array($callback, $params))) { return $r; } // we need to re-translate the urls to pathes, as they can eg. contain symlinks foreach($urls as $path => $url) { - if (isset($r[$url]) || isset($r[$url=Vfs::parse_url($url,PHP_URL_PATH)])) + if(isset($r[$url]) || isset($r[$url = Vfs::parse_url($url, PHP_URL_PATH)])) { $ret[$path] = $r[$url]; } @@ -607,7 +700,7 @@ class Base else { $time = null; - return $name($url,$time); + return $name($url, $time); } } return $ret; diff --git a/api/templates/default/etemplate2.css b/api/templates/default/etemplate2.css index 75a6f164dd..870c02ab0b 100644 --- a/api/templates/default/etemplate2.css +++ b/api/templates/default/etemplate2.css @@ -2264,7 +2264,6 @@ div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button { float: left; margin-right: 1em; background-position: 3px; - padding-left: 15px; outline: none; } div.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset button.right { diff --git a/infolog/js/app.ts b/infolog/js/app.ts index 71a0ac59ae..50b1d44c84 100644 --- a/infolog/js/app.ts +++ b/infolog/js/app.ts @@ -645,8 +645,7 @@ class InfologApp extends EgwApp */ getWindowTitle() { - var widget = this.et2.getWidgetById('info_subject'); - if(widget) return widget.options.value; + return this.et2.getValueById("info_subject"); } /** diff --git a/infolog/templates/default/index.xet b/infolog/templates/default/index.xet index 6c1c87953b..50e6818f34 100644 --- a/infolog/templates/default/index.xet +++ b/infolog/templates/default/index.xet @@ -75,7 +75,7 @@ - + diff --git a/mail/inc/class.mail_integration.inc.php b/mail/inc/class.mail_integration.inc.php index 00ee1b500f..14e927b82d 100644 --- a/mail/inc/class.mail_integration.inc.php +++ b/mail/inc/class.mail_integration.inc.php @@ -326,6 +326,11 @@ class mail_integration { } } + //consider all addresses in the header + foreach (['TO','CC','BCC', 'FROM'] as $h) + { + $mailcontent['mailaddress'] .= ','.$headers[$h]; + } // Convert addresses to email and personal $addresses = imap_rfc822_parse_adrlist($mailcontent['mailaddress'],''); foreach ($addresses as $address)