Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-07-19 14:15:52 -06:00
commit ad00156113
14 changed files with 197 additions and 122 deletions

View File

@ -1081,7 +1081,7 @@ class AdminApp extends EgwApp
if(egw.app('policy'))
{
import(egw.link('/policy/js/app.js?'+((new Date).valueOf()/86400|0).toString())).then(() => {
import(egw.link('/policy/js/app.min.js?'+((new Date).valueOf()/86400|0).toString())).then(() => {
if(typeof app.policy === 'undefined' || typeof app.policy.confirm === 'undefined')
{
app.policy = new app.classes.policy();

View File

@ -396,17 +396,14 @@ egwFnct.prototype.setValue = function(_value)
// check if we need a not yet included app.js object --> include it now and re-set when it arrives
else if (i === 1 && typeof app.classes[parts[1]] === "undefined")
{
var self = this;
return new Promise(function(resolve)
{
egw.includeJS("/"+parts[1]+"/js/app.js", function ()
{
if(typeof app.classes[parts[i]] !== "undefined")
return import(egw.webserverUrl+"/"+parts[1]+"/js/app.min.js?"+((new Date).valueOf()/86400|0).toString())
.then(() => {
if(typeof app.classes[parts[i]] === "undefined")
{
resolve(this.setValue(_value));
throw new Error("app.classes."+parts[i]+" not found!");
}
}.bind(this), self, egw.webserverUrl);
}.bind(this));
this.setValue(_value);
});
}
// check if we need a not yet instantiated app.js object --> instantiate it now
else if (i === 1 && typeof app.classes[parts[1]] === "function")

View File

@ -47,26 +47,35 @@ export function et2_loadXMLFromURL(_url : string, _callback? : Function, _contex
{
win = egw.top;
}
return win.jQuery.ajax({
// we add the full url (protocol and domain) as sometimes just the path
// gives a CSP error interpreting it as file:///path
// (if there are a enough 404 errors in html content ...)
url: (_url[0]=='/' ? location.protocol+'//'+location.host : '')+_url,
context: _context,
type: 'GET',
dataType: 'xml',
success: function(_data, _status, _xmlhttp){
if (typeof _callback === 'function') {
_callback.call(_context, _data.documentElement);
// we add the full url (protocol and domain) as sometimes just the path
// gives a CSP error interpreting it as file:///path
// (if there are a enough 404 errors in html content ...)
return win.fetch((_url[0] === '/' ? location.protocol+'//'+location.host : '')+_url, {
method: 'GET'
})
.then((response) => {
if (!response.ok) {
throw response;
}
},
error: function(_xmlhttp, _err) {
egw().debug('error', 'Loading eTemplate from '+_url+' failed! '+_xmlhttp.status+' '+_xmlhttp.statusText);
return response.text();
})
.then((xml) => {
const parser = new window.DOMParser();
return parser.parseFromString( xml, "text/xml" );
})
.then((xmldoc) => {
if (typeof _callback === 'function') {
_callback.call(_context, xmldoc.children[0]);
}
return xmldoc.children[0];
})
.catch((_err) => {
egw().message('Loading eTemplate from '+_url+' failed!'+"\n\n"+
(typeof _err.stack !== 'undefined' ? _err.stack : _err.status+' '+_err.statusText), 'error');
if(typeof _fail_callback === 'function') {
_fail_callback.call(_context, _err);
}
}
});
});
}
export function et2_directChildrenByTagName(_node : HTMLElement, _tagName : String) : HTMLElement[]

View File

@ -1469,60 +1469,40 @@ export class et2_selectbox extends et2_inputWidget
// normalize options by removing trailing commas
options_string = options_string.replace(/,+$/, '');
var cache_id = widget._type+'_'+options_string;
var cache_owner = (
// Todo: @new-js-loader et2_selectbox is no longer instanciated globaly --> caching needs to be fixed
et2_selectbox
/*egw.window.et2_selectbox ?
egw.window.et2_selectbox :
egw(window).window.et2_selectbox*/
).type_cache;
var cache = cache_owner[cache_id];
const cache_id = widget._type+'_'+options_string;
const cache_owner = egw.getCache('et2_selectbox');
let cache = cache_owner[cache_id];
// Options for a selectbox in a nextmatch must be returned now, as the
// widget we have is not enough to set the options later.
var in_nextmatch = false;
if(typeof cache === 'undefined' || typeof cache.length === 'undefined')
{
var parent = widget._parent;
while(parent && !in_nextmatch)
{
in_nextmatch = parent && parent._type && parent._type === 'nextmatch';
parent = parent._parent;
}
}
if (typeof cache == 'undefined' || in_nextmatch)
if (typeof cache === 'undefined')
{
// Fetch with json instead of jsonq because there may be more than
// one widget listening for the response by the time it gets back,
// and we can't do that when it's queued.
var req = egw.json(
const req = egw.json(
'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options',
[widget._type,options_string,attrs.value]
).sendRequest(!in_nextmatch);
).sendRequest();
if(typeof cache === 'undefined')
{
cache_owner[cache_id] = req;
}
cache = req;
}
if(typeof cache.done == 'function')
if (typeof cache.then === 'function')
{
// pending, wait for it
cache.done(jQuery.proxy(function(response) {
cache.then((response) => {
cache = cache_owner[cache_id] = response.response[0].data||undefined;
// Set select_options in attributes in case we get a resonse before
// Set select_options in attributes in case we get a response before
// the widget is finished loading (otherwise it will re-set to {})
attrs.select_options = cache;
egw.window.setTimeout(jQuery.proxy(function() {
// Avoid errors if widget is destroyed before the timeout
if(this.widget && typeof this.widget.id !== 'undefined')
{
this.widget.set_select_options(et2_selectbox.find_select_options(this.widget,{}, this.widget.options));
}
},this),1);
},{widget:widget,cache_id:cache_id}));
// Avoid errors if widget is destroyed before the timeout
if (widget && typeof widget.id !== 'undefined')
{
widget.set_select_options(et2_selectbox.find_select_options(widget,{}, widget.options));
}
});
return [];
}
else

View File

@ -75,9 +75,6 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
this.sender = _sender ? _sender : null;
this.egw = _egw;
// We currently don't have a request object
this.request = null;
// Some variables needed for notification about a JS files done loading
this.onLoadFinish = null;
this.jsFiles = 0;
@ -191,7 +188,7 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
* @param {string} method ='POST' allow to eg. use a (cachable) 'GET' request instead of POST
* @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error
*
* @return {jqXHR|boolean} jQuery jqXHR request object or for async==="keepalive" boolean is returned
* @return {Promise|boolean} Promise or for async==="keepalive" boolean is returned
*/
json_request.prototype.sendRequest = function(async, method, error)
{
@ -203,13 +200,13 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
if (typeof method === 'undefined') method = 'POST';
// Assemble the complete request
var request_obj = JSON.stringify({
const request_obj = JSON.stringify({
request: {
parameters: this.parameters
}
});
// send with keepalive===true or sendBeacon to be used in beforeunload event
// send with keepalive===true for sendBeacon to be used in beforeunload event
if (this.async === "keepalive" && typeof navigator.sendBeacon !== "undefined")
{
const data = new FormData();
@ -218,53 +215,94 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
return navigator.sendBeacon(this.url, data);
}
// Send the request via AJAX using the jquery ajax function
// we need to use jQuery of window of egw object, as otherwise the one from main window is used!
// (causing eg. apply from server with app.$app.method to run in main window instead of popup)
this.request = (this.egw.window?this.egw.window.jQuery:jQuery).ajax({
url: this.url,
async: this.async,
context: this,
// only POST can send JSON as direct payload, GET can not
data: method === 'GET' ? { json_data: request_obj } : request_obj,
contentType: method === 'GET' ? false : 'application/json',
dataType: 'json',
type: method,
success: this.handleResponse,
jsonp: false,
error: error || this.handleError
});
let url = this.url;
let init = {
method: method
}
if (method === 'GET')
{
url += (url.indexOf('?') === -1 ? '?' : '&') + new URLSearchParams({ json_data: request_obj });
}
else
{
init.headers = { 'Content-Type': 'application/json'};
init.body = request_obj;
}
let promise;
if (this.async)
{
promise = (this.egw.window?this.egw.window:window).fetch(url, init)
.then((response) => {
if (!response.ok) {
throw response;
}
return response.json();
})
.then((data) => this.handleResponse(data) || data)
.catch((_err) => {
(error || this.handleError).call(this, _err)
});
}
else
{
console.trace("Synchronous AJAX request detected", this);
const request = new XMLHttpRequest();
request.open(method, url, false);
if (method !== 'GET') request.setRequestHeader('Content-Type', 'application/json');
request.send(init.body);
if (request.status >= 200 && request.status < 300)
{
const json = JSON.parse(request.responseText);
promise = Promise.resolve(this.handleResponse(json) || json);
}
else
{
(error || this.handleError).call(this, request, 'error')
}
}
// compatibility with jQuery.ajax
if (promise && typeof promise.then === 'function') promise.done = promise.then;
return this.request;
return promise;
};
/**
* Default error callback displaying error via egw.message
*
* @param {XMLHTTP} _xmlhttp
* @param {XMLHTTP|Response} response
* @param {string} _err
*/
json_request.prototype.handleError = function(_xmlhttp, _err) {
json_request.prototype.handleError = function(response, _err) {
// Don't error about an abort
if(_err !== 'abort')
{
// for fetch Response get json, as it's used below
if (typeof response.headers === 'object' && response.headers.get('Content-Type') === 'application/json')
{
return response.json().then((json) => {
response.responseJSON = json;
this.handleError(response, 'error');
})
}
const date = typeof response.headers === 'object' ? 'Date: '+response.headers.get('Date') :
(typeof response.getAllResponseHeaders === 'function' ? response.getAllResponseHeaders().match(/^Date:.*$/mi)[0] : null) ||
'Date: '+(new Date).toString();
this.egw.message.call(this.egw,
this.egw.lang('A request to the EGroupware server returned with an error')+
': '+_xmlhttp.statusText+' ('+_xmlhttp.status+")\n\n"+
': '+response.statusText+' ('+response.status+")\n\n"+
this.egw.lang('Please reload the EGroupware desktop (F5 / Cmd+r).')+"\n"+
this.egw.lang('If the error persists, contact your administrator for help and ask to check the error-log of the webserver.')+
"\n\nURL: "+this.url+"\n"+
(_xmlhttp.getAllResponseHeaders() ? (_xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi) ? _xmlhttp.getAllResponseHeaders().match(/^Date:.*$/mi)[0]:''):'')+
"\n\nURL: "+this.url+"\n"+date+
// if EGroupware send JSON payload with error, errno show it here too
(_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' && _xmlhttp.responseJSON.error ?
"\nError: "+_xmlhttp.responseJSON.error+' ('+_xmlhttp.responseJSON.errno+')' : '')
(_err === 'error' && response.status === 400 && typeof response.responseJSON === 'object' && response.responseJSON.error ?
"\nError: "+response.responseJSON.error+' ('+response.responseJSON.errno+')' : '')
);
this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, _xmlhttp.status, _xmlhttp.statusText, _xmlhttp.responseJSON);
this.egw.debug('error', 'Ajax request to', this.url, ' failed: ', _err, response.status, response.statusText, response.responseJSON);
// check of unparsable JSON on server-side, which might be caused by some network problem --> resend max. twice
if (_err === 'error' && _xmlhttp.status === 400 && typeof _xmlhttp.responseJSON === 'object' &&
_xmlhttp.responseJSON.errno && _xmlhttp.responseJSON.error.substr(0, 5) === 'JSON ')
if (_err === 'error' && response.status === 400 && typeof response.responseJSON === 'object' &&
response.responseJSON.errno && response.responseJSON.error.substr(0, 5) === 'JSON ')
{
// ToDo: resend request max. twice
}
@ -378,12 +416,11 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
}
}
// Call request callback, if provided
if(this.callback != null && !only_data)
if(typeof this.callback === 'function' && !only_data)
{
this.callback.call(this.context,res);
}
}
this.request = null;
};
var json = {
@ -538,9 +575,9 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
// check if we need a not yet included app.js object --> include it now and return a Promise
else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'undefined')
{
return import(this.webserverUrl+'/'+parts[1]+'/js/app.js?'+((new Date).valueOf()/86400|0).toString())
return import(this.webserverUrl+'/'+parts[1]+'/js/app.min.js?'+((new Date).valueOf()/86400|0).toString())
.then(() => this.applyFunc(_func, args, _context),
(err) => {console.error("Failure loading /"+parts[1]+'/js/app.js' + " (" + err + ")\nAborting.")});
(err) => {console.error("Failure loading /"+parts[1]+'/js/app.min.js' + " (" + err + ")\nAborting.")});
}
// check if we need a not yet instantiated app.js object --> instantiate it now
else if (i == 1 && parts[0] == 'app' && typeof app.classes[parts[1]] === 'function')

View File

@ -53,31 +53,47 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function()
/**
* Query an EGroupware user preference
*
* If a prefernce is not already loaded (only done for "common" by default), it is synchroniosly queryed from the server!
* If a preference is not already loaded (only done for "common" by default),
* it is synchronously queried from the server, if no _callback parameter is given!
*
* @param {string} _name name of the preference, eg. 'dateformat', or '*' to get all the application's preferences
* @param {string} _app default 'common'
* @param {function|false|undefined} _callback optional callback, if preference needs loading first
* if false given and preference is not loaded, undefined is return and no (synchronious) request is send to server
* @param {function|boolean|undefined} _callback optional callback, if preference needs loading first
* - default/undefined: preference is synchronously queried, if not loaded, and returned
* - function: if loaded, preference is returned, if not false and callback is called once it's loaded
* - true: a promise for the preference is returned
* - false: if preference is not loaded, undefined is return and no (synchronous) request is send to server
* @param {object} _context context for callback
* @return string|bool preference value or false, if callback given and preference not yet loaded
* @return Promise|object|string|bool (Promise for) preference value or false, if callback given and preference not yet loaded
*/
preference: function(_name, _app, _callback, _context)
{
if (typeof _app == 'undefined') _app = 'common';
if (typeof _app === 'undefined') _app = 'common';
if (typeof prefs[_app] == 'undefined')
if (typeof prefs[_app] === 'undefined')
{
if (_callback === false) return undefined;
var request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context);
request.sendRequest(typeof _callback == 'function', 'GET'); // use synchronous (cachable) GET request
if (typeof prefs[_app] == 'undefined') prefs[_app] = {};
if (typeof _callback == 'function') return false;
const request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context);
const promise = request.sendRequest(typeof _callback !== 'undefined', 'GET');
if (typeof prefs[_app] === 'undefined') prefs[_app] = {};
if (_callback === true) return promise.then(() => this.preference(_name, _app));
if (typeof _callback === 'function') return false;
}
if (_name == "*") return typeof prefs[_app] ==='object' ? jQuery.extend({},prefs[_app]) : prefs[_app];
return typeof prefs[_app][_name] === 'object' && prefs[_app][_name] !== null ?
jQuery.extend({},prefs[_app][_name]) : prefs[_app][_name];
let ret;
if (_name === "*")
{
ret = typeof prefs[_app] === 'object' ? jQuery.extend({}, prefs[_app]) : prefs[_app];
}
else
{
ret = typeof prefs[_app][_name] === 'object' && prefs[_app][_name] !== null ?
jQuery.extend({}, prefs[_app][_name]) : prefs[_app][_name];
}
if (_callback === true)
{
return Promise.resolve(ret);
}
return ret;
},
/**

View File

@ -145,9 +145,27 @@ egw.extend('utils', egw.MODULE_GLOBAL, function()
var uid_counter = 0;
/**
* Global cache shared between all EGroupware windows
* @type {{}}
*/
const cache = {};
// Create the utils object which contains references to all functions
// covered by it.
var utils = {
/**
* Get a cache object shared between all EGroupware windows
*
* @param {string} _name unique name for the cache-object
* @return {*}
*/
getCache: function(_name)
{
if (typeof cache[_name] === 'undefined') cache[_name] = {};
return cache[_name];
},
ajaxUrl: function(_menuaction) {
if(_menuaction.indexOf('menuaction=') >= 0)

View File

@ -239,6 +239,11 @@ class Etemplate extends Etemplate\Widget\Template
{
Framework::includeJS($path);
}
// if app has no app.ts/js, we need to load etemplate2.js, otherwise popups wont work!
else
{
Framework::includeJS('/api/js/etemplate/etemplate2.js');
}
// Category styles
Categories::css($app);

View File

@ -119,7 +119,7 @@ class Bundle
$mod = $min_mod;
}
// use cache-buster only for entry-points / app.js, as the have no hash
if (preg_match('#/js/app(\.min)?\.js$#', $file))
if (preg_match('#/js/(app(\.min)?|etemplate/etemplate2)\.js$#', $file))
{
$to_include[$file] = $path.'?'.$mod.($query ? '&'.$query : '');
}

View File

@ -636,6 +636,7 @@ class WebDAV extends HTTP_WebDAV_Server_Filesystem
{
if ($_SERVER['REQUEST_METHOD'] == 'GET' && (($this->force_download = strpos($_SERVER['REQUEST_URI'],'?download')) !== false))
{
$_SERVER['ORIG_REQUEST_URI'] = $_SERVER['REQUEST_URI'];
$_SERVER['REQUEST_URI'] = substr($_SERVER['REQUEST_URI'],0,$this->force_download);
}
parent::__construct();

View File

@ -30,10 +30,16 @@ include '../header.inc.php';
$GLOBALS['egw']->session->commit_session();
// use an etag over config and link-registry
$preferences = json_encode($GLOBALS['egw_info']['user']['preferences']['common']);
$ab_preferences = json_encode($GLOBALS['egw_info']['user']['preferences']['addressbook']);
$preferences['common'] = $GLOBALS['egw_info']['user']['preferences']['common'];
foreach(['addressbook', 'notifications', 'status', 'filemanager'] as $app)
{
if (!empty($GLOBALS['egw_info']['user']['apps'][$app]))
{
$preferences[$app] = $GLOBALS['egw_info']['user']['preferences'][$app];
}
}
$user = $GLOBALS['egw']->accounts->json($GLOBALS['egw_info']['user']['account_id']);
$etag = '"'.md5($preferences.$ab_preferences.$user).'"';
$etag = '"'.md5(json_encode($preferences).$user).'"';
// headers to allow caching, egw_framework specifies etag on url to force reload, even with Expires header
Api\Session::cache_control(86400); // cache for 1 day
@ -47,9 +53,11 @@ if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $
exit;
}
$content = 'egw.set_preferences('.$preferences.", 'common', egw && egw.window !== window);\n";
$content .= 'egw.set_preferences('.$ab_preferences.", 'addressbook', egw && egw.window !== window);\n";
$content .= 'egw.set_user('.$user.", egw && egw.window !== window);\n";
$content = 'egw.set_user('.$user.", egw && egw.window !== window);\n";
foreach($preferences as $app => $data)
{
$content .= 'egw.set_preferences('.json_encode($data).', '.json_encode($app).", egw && egw.window !== window);\n";
}
// we run our own gzip compression, to set a correct Content-Length of the encoded content
if (in_array('gzip', explode(',',$_SERVER['HTTP_ACCEPT_ENCODING'])) && function_exists('gzencode'))

View File

@ -778,7 +778,7 @@ class InfologApp extends EgwApp
if (!app.stylite)
{
import(egw.webserverUrl+'/stylite/js/app.js?'+((new Date).valueOf()/86400|0).toString()).then(() =>
import(egw.webserverUrl+'/stylite/js/app.min.js?'+((new Date).valueOf()/86400|0).toString()).then(() =>
{
app.stylite = new app.classes.stylite;
app.stylite.et2 = this.et2;

View File

@ -908,7 +908,10 @@
var self = notifications;
var langRequire = jQuery('#notifications_script_id').attr('data-langRequire');
egw.langRequire(window, [JSON.parse(langRequire)]).then(()=>
Promise.all([
egw.langRequire(window, [JSON.parse(langRequire)]),
egw.preference('notification_chain','notifications', true)
]).then(() =>
{
var $egwpopup_fw = jQuery('#topmenu_info_notifications');
switch (egw.preference('notification_chain','notifications'))

View File

@ -31,6 +31,7 @@ const config = {
"pixelegg/js/fw_pixelegg.min": "pixelegg/js/fw_pixelegg.js",
"pixelegg/js/fw_mobile.min": "pixelegg/js/fw_mobile.js",
"api/js/jsapi/egw.min": "api/js/jsapi/egw_modules.js",
"api/js/etemplate/etemplate2": "api/js/etemplate/etemplate2.ts",
// app.ts/js are added automatic by addAppsConfig() below
},
@ -158,7 +159,7 @@ export default function addAppsConfig()
{
try {
statSync(file.name + '/js/app.ts');
config.input[file.name + '/js/app'] = file.name + '/js/app.ts';
config.input[file.name + '/js/app.min'] = file.name + '/js/app.ts';
}
catch (e) {
try {