ajax refactoring
This commit is contained in:
+170
-61
@@ -28,8 +28,29 @@
|
||||
|
||||
/** @module ajax */
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
let isConfigured = false;
|
||||
/** @type {number} */
|
||||
let defaultTimeout;
|
||||
/** @type {string} */
|
||||
let apiUrl;
|
||||
/** @type {Espo.Ajax~Handler} */
|
||||
let beforeSend;
|
||||
/** @type {Espo.Ajax~Handler} */
|
||||
let onSuccess;
|
||||
/** @type {Espo.Ajax~Handler} */
|
||||
let onError;
|
||||
/** @type {Espo.Ajax~Handler} */
|
||||
let onTimeout;
|
||||
|
||||
/**
|
||||
* @callback Espo.Ajax~Handler
|
||||
* @param {XMLHttpRequest} [xhr]
|
||||
* @param {Object.<string, *>} [options]
|
||||
*/
|
||||
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* Functions for API HTTP requests.
|
||||
*/
|
||||
@@ -44,40 +65,130 @@ const Ajax = Espo.Ajax = {
|
||||
* @property {Object.<string, string>} [headers] A request headers.
|
||||
* @property {'json'|'text'} [dataType] A data type.
|
||||
* @property {string} [contentType] A content type.
|
||||
* @property {boolean} [fullResponse] To resolve with `module:ajax.XhrWrapper`.
|
||||
* @property {boolean} [resolveWithXhr] To resolve with `XMLHttpRequest`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request.
|
||||
*
|
||||
* @param {string} url An URL.
|
||||
* @param {string} method An HTTP method.
|
||||
* @param {'GET'|'POST'|'PUT'|'DELETE'|'PATCH'|'OPTIONS'} method An HTTP method.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {AjaxPromise<any>}
|
||||
* @returns {AjaxPromise<any, XMLHttpRequest>}
|
||||
*/
|
||||
request: function (url, method, data, options) {
|
||||
options = options || {};
|
||||
|
||||
options.type = method;
|
||||
options.url = url;
|
||||
let timeout = 'timeout' in options ? options.timeout : defaultTimeout;
|
||||
let contentType = options.contentType || 'application/json';
|
||||
let body;
|
||||
|
||||
if (data) {
|
||||
options.data = data;
|
||||
if (options.data && !data) {
|
||||
data = options.data;
|
||||
}
|
||||
|
||||
if (!['GET', 'OPTIONS'].includes(method) && data) {
|
||||
body = data;
|
||||
|
||||
if (contentType === 'application/json' && typeof data !== 'string') {
|
||||
body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (apiUrl) {
|
||||
url = Espo.Utils.trimSlash(apiUrl) + '/' + url;
|
||||
}
|
||||
|
||||
let urlObj = new URL(baseUrl + url);
|
||||
|
||||
if (method === 'GET' && data) {
|
||||
for (let key in data) {
|
||||
let value = data[key];
|
||||
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
urlObj.searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let xhr = new Xhr();
|
||||
xhr.timeout = timeout;
|
||||
xhr.open(method, urlObj);
|
||||
xhr.setRequestHeader('Content-Type', contentType);
|
||||
|
||||
if (options.headers) {
|
||||
for (let key in options.headers) {
|
||||
xhr.setRequestHeader(key, options.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (beforeSend) {
|
||||
beforeSend(xhr, options);
|
||||
}
|
||||
|
||||
let promiseWrapper = {};
|
||||
|
||||
let promise = new AjaxPromise((resolve, reject) => {
|
||||
let xhr = $.ajax(options);
|
||||
const onErrorGeneral = (isTimeout) => {
|
||||
if (options.error) {
|
||||
options.error(xhr, options);
|
||||
}
|
||||
|
||||
xhr
|
||||
.then((response, status, xhr) => {
|
||||
let obj = options.fullResponse ? new XhrWrapper(xhr) : response;
|
||||
reject(xhr, options);
|
||||
|
||||
resolve(obj);
|
||||
})
|
||||
.fail(xhr => reject(xhr));
|
||||
if (isTimeout) {
|
||||
if (onTimeout) {
|
||||
onTimeout(xhr, options);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
onError(xhr, options);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = () => onErrorGeneral(true);
|
||||
xhr.onerror = () => onErrorGeneral();
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 400) {
|
||||
onErrorGeneral();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let response = xhr.responseText;
|
||||
|
||||
if ((options.dataType || 'json') === 'json') {
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Could not parse API response.');
|
||||
|
||||
onErrorGeneral();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.success) {
|
||||
options.success(response);
|
||||
}
|
||||
|
||||
onSuccess(xhr, options);
|
||||
|
||||
if (options.resolveWithXhr) {
|
||||
response = xhr;
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
}
|
||||
|
||||
xhr.send(body);
|
||||
|
||||
if (promiseWrapper.promise) {
|
||||
promiseWrapper.promise.xhr = xhr;
|
||||
@@ -100,7 +211,7 @@ const Ajax = Espo.Ajax = {
|
||||
* @param {string} url An URL.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<any, XMLHttpRequest>}
|
||||
*/
|
||||
postRequest: function (url, data, options) {
|
||||
if (data) {
|
||||
@@ -116,7 +227,7 @@ const Ajax = Espo.Ajax = {
|
||||
* @param {string} url An URL.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<any, XMLHttpRequest>}
|
||||
*/
|
||||
patchRequest: function (url, data, options) {
|
||||
if (data) {
|
||||
@@ -132,7 +243,7 @@ const Ajax = Espo.Ajax = {
|
||||
* @param {string} url An URL.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<any, XMLHttpRequest>}
|
||||
*/
|
||||
putRequest: function (url, data, options) {
|
||||
if (data) {
|
||||
@@ -148,7 +259,7 @@ const Ajax = Espo.Ajax = {
|
||||
* @param {string} url An URL.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<any, XMLHttpRequest>}
|
||||
*/
|
||||
deleteRequest: function (url, data, options) {
|
||||
if (data) {
|
||||
@@ -164,11 +275,37 @@ const Ajax = Espo.Ajax = {
|
||||
* @param {string} url An URL.
|
||||
* @param {*} [data] Data.
|
||||
* @param {Espo.Ajax~Options & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise<any>}
|
||||
* @returns {Promise<any, XMLHttpRequest>}
|
||||
*/
|
||||
getRequest: function (url, data, options) {
|
||||
return /** @type {Promise<any>} */ Ajax.request(url, 'GET', data, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param {{
|
||||
* apiUrl: string,
|
||||
* timeout: number,
|
||||
* beforeSend: Espo.Ajax~Handler,
|
||||
* onSuccess: Espo.Ajax~Handler,
|
||||
* onError: Espo.Ajax~Handler,
|
||||
* onTimeout: Espo.Ajax~Handler,
|
||||
* }} options Options.
|
||||
*/
|
||||
configure: function (options) {
|
||||
if (isConfigured) {
|
||||
throw new Error("Ajax is already configured.");
|
||||
}
|
||||
|
||||
apiUrl = options.apiUrl;
|
||||
defaultTimeout = options.timeout;
|
||||
beforeSend = options.beforeSend;
|
||||
onSuccess = options.onSuccess;
|
||||
onError = options.onError;
|
||||
onTimeout = options.onTimeout;
|
||||
|
||||
isConfigured = true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -177,18 +314,24 @@ const Ajax = Espo.Ajax = {
|
||||
class AjaxPromise extends Promise {
|
||||
|
||||
/**
|
||||
* @type {JQueryXHR|null}
|
||||
* @type {XMLHttpRequest|null}
|
||||
* @internal
|
||||
*/
|
||||
xhr = null
|
||||
|
||||
isAborted = false
|
||||
|
||||
/** @deprecated Use `catch`. */
|
||||
/**
|
||||
* @deprecated Use `catch`.
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
fail(...args) {
|
||||
return this.catch(args[0]);
|
||||
}
|
||||
/** @deprecated Use `then`. */
|
||||
/**
|
||||
* @deprecated Use `then`
|
||||
* @todo Remove in v9.0.
|
||||
*/
|
||||
done(...args) {
|
||||
return this.then(args[0]);
|
||||
}
|
||||
@@ -232,47 +375,13 @@ class AjaxPromise extends Promise {
|
||||
}
|
||||
|
||||
/**
|
||||
* @name module:ajax.XhrWrapper
|
||||
* @name module:ajax.Xhr
|
||||
*/
|
||||
class XhrWrapper {
|
||||
|
||||
class Xhr extends XMLHttpRequest {
|
||||
/**
|
||||
* @param {JQueryXHR} xhr
|
||||
* To be set in an error handler to bypass default handling.
|
||||
*/
|
||||
constructor(xhr) {
|
||||
this.xhr = xhr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @return {string}
|
||||
*/
|
||||
getResponseHeader(name) {
|
||||
return this.xhr.getResponseHeader(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Number}
|
||||
*/
|
||||
getStatus() {
|
||||
return this.xhr.status;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* @return {*}
|
||||
*/
|
||||
getResponseParsedBody() {
|
||||
return this.xhr.responseJSON;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
getResponseBody() {
|
||||
return this.xhr.responseText;
|
||||
}
|
||||
errorIsHandled = false
|
||||
}
|
||||
|
||||
export default Ajax;
|
||||
|
||||
+102
-68
@@ -882,8 +882,8 @@ class App {
|
||||
if (arr.length > 1) {
|
||||
logoutWait = this.appParams.logoutWait || false;
|
||||
|
||||
Ajax.postRequest('App/destroyAuthToken', {token: arr[1]}, {fullResponse: true})
|
||||
.then(xhr => {
|
||||
Ajax.postRequest('App/destroyAuthToken', {token: arr[1]}, {resolveWithXhr: true})
|
||||
.then(/** XMLHttpRequest */xhr => {
|
||||
let redirectUrl = xhr.getResponseHeader('X-Logout-Redirect-Url');
|
||||
|
||||
if (redirectUrl) {
|
||||
@@ -1069,87 +1069,77 @@ class App {
|
||||
* @private
|
||||
*/
|
||||
setupAjax() {
|
||||
$.ajaxSetup({
|
||||
beforeSend: (xhr, /** JQueryAjaxSettings & Object.<string, *> */ options) => {
|
||||
if (!options.local && this.apiUrl) {
|
||||
options.url = Utils.trimSlash(this.apiUrl) + '/' + options.url;
|
||||
}
|
||||
/**
|
||||
* @param {XMLHttpRequest} xhr
|
||||
*/
|
||||
const beforeSend = (xhr) => {
|
||||
if (this.auth !== null) {
|
||||
xhr.setRequestHeader('Authorization', 'Basic ' + this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization', this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization-By-Token', 'true');
|
||||
}
|
||||
|
||||
if (!options.local && this.basePath !== '') {
|
||||
options.url = this.basePath + options.url;
|
||||
}
|
||||
|
||||
if (this.auth !== null) {
|
||||
xhr.setRequestHeader('Authorization', 'Basic ' + this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization', this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization-By-Token', 'true');
|
||||
}
|
||||
|
||||
if (this.anotherUser !== null) {
|
||||
xhr.setRequestHeader('X-Another-User', this.anotherUser);
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
timeout: this.ajaxTimeout,
|
||||
contentType: 'application/json',
|
||||
});
|
||||
if (this.anotherUser !== null) {
|
||||
xhr.setRequestHeader('X-Another-User', this.anotherUser);
|
||||
}
|
||||
};
|
||||
|
||||
let appTimestampChangeProcessed = false;
|
||||
|
||||
$(document).ajaxSuccess((e, xhr, options) => {
|
||||
/**
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @param {Object.<string, *>} options
|
||||
*/
|
||||
const onSuccess = (xhr, options) => {
|
||||
let appTimestampHeader = xhr.getResponseHeader('X-App-Timestamp');
|
||||
|
||||
if (appTimestampHeader && !appTimestampChangeProcessed) {
|
||||
let appTimestamp = parseInt(appTimestampHeader);
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
let bypassAppReload = options.bypassAppReload;
|
||||
|
||||
if (
|
||||
this.appTimestamp &&
|
||||
appTimestamp !== this.appTimestamp &&
|
||||
!bypassAppReload
|
||||
) {
|
||||
appTimestampChangeProcessed = true;
|
||||
|
||||
Ui
|
||||
.confirm(
|
||||
this.language.translate('confirmAppRefresh', 'messages'),
|
||||
{
|
||||
confirmText: this.language.translate('Refresh'),
|
||||
cancelText: this.language.translate('Cancel'),
|
||||
backdrop: 'static',
|
||||
confirmStyle: 'success',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.postMessage('reload');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!appTimestampHeader || appTimestampChangeProcessed) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ajaxError((e, xhr, options) => {
|
||||
// To process after a promise-catch.
|
||||
let appTimestamp = parseInt(appTimestampHeader);
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
let bypassAppReload = options.bypassAppReload;
|
||||
|
||||
if (
|
||||
this.appTimestamp &&
|
||||
appTimestamp !== this.appTimestamp &&
|
||||
!bypassAppReload
|
||||
) {
|
||||
appTimestampChangeProcessed = true;
|
||||
|
||||
Ui
|
||||
.confirm(
|
||||
this.language.translate('confirmAppRefresh', 'messages'),
|
||||
{
|
||||
confirmText: this.language.translate('Refresh'),
|
||||
cancelText: this.language.translate('Cancel'),
|
||||
backdrop: 'static',
|
||||
confirmStyle: 'success',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.postMessage('reload');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:ajax.Xhr} xhr
|
||||
* @param {Object.<string, *>} options
|
||||
*/
|
||||
const onError = (xhr, options) => {
|
||||
setTimeout(() => {
|
||||
// noinspection JSUnresolvedReference
|
||||
if (xhr.errorIsHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (xhr.status) {
|
||||
case 0:
|
||||
if (xhr.statusText === 'timeout') {
|
||||
Ui.error(this.language.translate('Timeout'), true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 200:
|
||||
Ui.error(this.language.translate('Bad server response'));
|
||||
|
||||
@@ -1227,6 +1217,50 @@ class App {
|
||||
console.error('Server side error ' + xhr.status + ': ' + statusReason);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
Ui.error(this.language.translate('Timeout'), true);
|
||||
};
|
||||
|
||||
Ajax.configure({
|
||||
apiUrl: this.basePath + this.apiUrl,
|
||||
timeout: this.ajaxTimeout,
|
||||
beforeSend: beforeSend,
|
||||
onSuccess: onSuccess,
|
||||
onError: onError,
|
||||
onTimeout: onTimeout,
|
||||
});
|
||||
|
||||
// For backward compatibility.
|
||||
// @todo Remove in v9.0.
|
||||
$.ajaxSetup({
|
||||
beforeSend: (xhr, options) => {
|
||||
console.error(`$.ajax is deprecated, support will be removed in v9.0. Use Espo.Ajax instead.`);
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
if (!options.local && this.apiUrl) {
|
||||
options.url = Utils.trimSlash(this.apiUrl) + '/' + options.url;
|
||||
}
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
if (!options.local && this.basePath !== '') {
|
||||
options.url = this.basePath + options.url;
|
||||
}
|
||||
|
||||
if (this.auth !== null) {
|
||||
xhr.setRequestHeader('Authorization', 'Basic ' + this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization', this.auth);
|
||||
xhr.setRequestHeader('Espo-Authorization-By-Token', 'true');
|
||||
}
|
||||
|
||||
if (this.anotherUser !== null) {
|
||||
xhr.setRequestHeader('X-Another-User', this.anotherUser);
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
timeout: this.ajaxTimeout,
|
||||
contentType: 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@ class OidcLoginHandler extends LoginHandler {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.processWithData(data)
|
||||
.then((code, nonce) => {
|
||||
.then(info => {
|
||||
let code = info.code;
|
||||
let nonce = info.nonce;
|
||||
|
||||
let authString = Base64.encode('**oidc:' + code);
|
||||
|
||||
let headers = {
|
||||
@@ -75,7 +78,7 @@ class OidcLoginHandler extends LoginHandler {
|
||||
* prompt: 'login'|'consent'|'select_account',
|
||||
* maxAge: ?Number,
|
||||
* }} data
|
||||
* @return {Promise}
|
||||
* @return {Promise<{code: string, nonce: string}>}
|
||||
*/
|
||||
processWithData(data) {
|
||||
let state = (Math.random() + 1).toString(36).substring(7);
|
||||
@@ -114,7 +117,7 @@ class OidcLoginHandler extends LoginHandler {
|
||||
* @param {string} url
|
||||
* @param {string} state
|
||||
* @param {string} nonce
|
||||
* @return {Promise}
|
||||
* @return {Promise<{code: string, nonce: string}>}
|
||||
*/
|
||||
processWindow(url, state, nonce) {
|
||||
let proxy = window.open(url, 'ConnectWithOAuth', 'location=0,status=0,width=800,height=800');
|
||||
@@ -177,7 +180,10 @@ class OidcLoginHandler extends LoginHandler {
|
||||
window.clearInterval(interval);
|
||||
proxy.close();
|
||||
|
||||
resolve(parsedData.code, nonce);
|
||||
resolve({
|
||||
code: parsedData.code,
|
||||
nonce: nonce,
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
@@ -1050,16 +1050,16 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a module or multiple modules.
|
||||
* Require a module.
|
||||
*
|
||||
* @param {...string} id A module or modules to require.
|
||||
* @returns {Promise<unknown>}
|
||||
* @param {string} id A module to require.
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
requirePromise(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.require(
|
||||
id,
|
||||
(...args) => resolve(...args),
|
||||
arg => resolve(arg),
|
||||
() => reject()
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -492,7 +492,7 @@ class Model {
|
||||
* patch?: boolean,
|
||||
* wait?: boolean,
|
||||
* } & Object.<string, *>} [options] Options.
|
||||
* @returns {Promise}
|
||||
* @returns {Promise<Object.<string, *>>}
|
||||
* @fires Model#sync
|
||||
* @copyright Credits to Backbone.js.
|
||||
*/
|
||||
|
||||
@@ -1087,9 +1087,6 @@ class BaseRecordView extends View {
|
||||
{
|
||||
patch: !model.isNew(),
|
||||
headers: headers,
|
||||
// Can't use a promise-catch, as it's called
|
||||
// after the default ajaxError callback.
|
||||
error: (m, xhr) => onError(xhr, reject),
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
@@ -1105,6 +1102,9 @@ class BaseRecordView extends View {
|
||||
model.trigger('after:save');
|
||||
|
||||
resolve();
|
||||
})
|
||||
.catch(xhr => {
|
||||
onError(xhr, reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1112,7 @@ class BaseRecordView extends View {
|
||||
/**
|
||||
* Handle a save error.
|
||||
*
|
||||
* @param {JQueryXHR} xhr XHR.
|
||||
* @param {module:ajax.Xhr} xhr XHR.
|
||||
* @param {module:views/record/base~saveOptions} [options] Options.
|
||||
*/
|
||||
handleSaveError(xhr, options) {
|
||||
|
||||
Reference in New Issue
Block a user