From 0afb9c16eab3f9c1fd069b350d4fa9fee4c0ec73 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Tue, 28 May 2024 08:47:35 +0000 Subject: [PATCH] allow abstract api to work with real promises instead of deferreds and use it, fixes #3643 Closes #3643 Merge request studip/studip!2885 --- .../javascripts/bootstrap/consultations.js | 4 +- resources/assets/javascripts/init.js | 2 +- .../assets/javascripts/lib/abstract-api.js | 40 +++++++++++++++++++ .../lib/{jsonapi.js => jsonapi.ts} | 8 ++-- resources/assets/javascripts/studip-ui.js | 4 +- resources/vue/components/MyCoursesTiles.vue | 6 +-- resources/vue/store/ContentModulesStore.js | 2 +- resources/vue/store/MyCoursesStore.js | 2 +- 8 files changed, 54 insertions(+), 14 deletions(-) rename resources/assets/javascripts/lib/{jsonapi.js => jsonapi.ts} (75%) diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js index ef79d9ca1e6..51ffa853085 100644 --- a/resources/assets/javascripts/bootstrap/consultations.js +++ b/resources/assets/javascripts/bootstrap/consultations.js @@ -10,9 +10,9 @@ $(document).on('click', '.consultation-delete-check:not(.ignore)', event => { } let requests = ids.map(id => { - return STUDIP.jsonapi.GET(`consultation-slots/${id}/bookings`).then(result => result.data.length); + return STUDIP.jsonapi.withPromises().get(`consultation-slots/${id}/bookings`).then(response => response.data.length); }); - $.when(...requests).done((...results) => { + Promise.all(requests).then((...results) => { if (results.some(result => result > 0)) { $(event.target).addClass('ignore').click().removeClass('ignore'); } else { diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 2d592be2ca4..fe20c9ae894 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -38,7 +38,7 @@ import HeaderMagic from './lib/header_magic.js'; import i18n from './lib/i18n.js'; import Instschedule from './lib/instschedule.js'; import InlineEditing from './lib/inline-editing.js'; -import JSONAPI, { jsonapi } from './lib/jsonapi.js'; +import JSONAPI, { jsonapi } from './lib/jsonapi.ts'; import JSUpdater from './lib/jsupdater.js'; import Lightbox from './lib/lightbox.js'; import Markup from './lib/markup.js'; diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js index 70a88dbbd60..95ae015c5f6 100644 --- a/resources/assets/javascripts/lib/abstract-api.js +++ b/resources/assets/javascripts/lib/abstract-api.js @@ -1,5 +1,20 @@ import Overlay from './overlay.js'; +class APIError extends Error +{ + static createWithJqXhr(message, jqXhr) { + const error = new APIError(message); + error.setJqXhr(jqXhr); + return error; + } + + jqXhr = null; + + setJqXhr(jqXhr) { + this.jqXhr = jqXhr; + } +} + class AbstractAPI { static get supportedMethods() { @@ -118,6 +133,31 @@ class AbstractAPI } }).join('&'); } + + withPromises() { + return new Proxy(this, { + get(target, prop, receiver) { + // This will allow http methods to be written as lowercase when called as methods + // (e.g. api.patch() instead of api.PATCH()) + if (target[prop] === undefined && AbstractAPI.supportedMethods.includes(prop.toUpperCase())) { + prop = prop.toUpperCase(); + } + + // Only handle calls to request methods + if (prop !== 'request') { + return Reflect.get(target, prop, receiver); + } + + // Return a wrapped promise that handles the deferred + return (url, options = {}) => new Promise((resolve, reject) => { + target[prop].apply(target, [url, options]).then( + (response) => resolve(response), + (jqXhr, textStatus, errorThrown) => reject(APIError.createWithJqXhr(errorThrown || textStatus, jqXhr)) + ); + }); + } + }) + } } // Create shortcut methods for easier access by method diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.ts similarity index 75% rename from resources/assets/javascripts/lib/jsonapi.js rename to resources/assets/javascripts/lib/jsonapi.ts index f3217bc6e6e..80176ccb6ca 100644 --- a/resources/assets/javascripts/lib/jsonapi.js +++ b/resources/assets/javascripts/lib/jsonapi.ts @@ -3,11 +3,11 @@ import AbstractAPI from './abstract-api.js'; // Actual JSONAPI object class JSONAPI extends AbstractAPI { - constructor(version = 1) { + constructor(version: number = 1) { super(`jsonapi.php/v${version}`); } - encodeData (data, method) { + encodeData (data: any, method: string): any { data = super.encodeData(data); if (['DELETE', 'GET', 'HEAD'].includes(method)) { @@ -21,11 +21,11 @@ class JSONAPI extends AbstractAPI return JSON.stringify(data); } - request (url, options = {}) { + request (url: string, options: any = {}) { options.contentType = 'application/vnd.api+json'; return super.request(url, options); } } export default JSONAPI; -export const jsonapi = new JSONAPI(); +export const jsonapi: JSONAPI = new JSONAPI(); diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js index f5812952a85..e611150f7b2 100644 --- a/resources/assets/javascripts/studip-ui.js +++ b/resources/assets/javascripts/studip-ui.js @@ -33,9 +33,9 @@ import eventBus from "./lib/event-bus.ts"; if (STUDIP.UI.restrictedDates[year] === undefined) { STUDIP.UI.restrictedDates[year] = {}; - STUDIP.jsonapi.GET('holidays', {data: { + STUDIP.jsonapi.withPromises().get('holidays', {data: { 'filter[year]': year - }}).done(response => { + }}).then(response => { // Since PHP will return an empty object as an array, // we need to check if (Array.isArray(response)) { diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue index 73f8aad308b..12b2627258a 100644 --- a/resources/vue/components/MyCoursesTiles.vue +++ b/resources/vue/components/MyCoursesTiles.vue @@ -136,7 +136,7 @@ export default { return this.shownColorPicker === course.id; }, changeColor(course, index) { - STUDIP.jsonapi.PATCH(`course-memberships/${course.id}_${this.userid}`, { + STUDIP.jsonapi.withPromises().patch(`course-memberships/${course.id}_${this.userid}`, { data: { data: { type: 'course-memberships', @@ -145,9 +145,9 @@ export default { } } } - }).done(() => { + }).then(() => { course.group = index; - }).always(() => { + }).finally(() => { this.shownColorPicker = null; }); }, diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js index 9dc4609630f..141a1780dc6 100644 --- a/resources/vue/store/ContentModulesStore.js +++ b/resources/vue/store/ContentModulesStore.js @@ -52,7 +52,7 @@ export default { attributes: { value: view === 'tiles' } }; - return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ; + return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } }) ; }, exchangeModules({ commit, state }, modules) { const order = modules.filter(module => module.active) diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js index 08c03891bef..af8e0fc4644 100644 --- a/resources/vue/store/MyCoursesStore.js +++ b/resources/vue/store/MyCoursesStore.js @@ -71,7 +71,7 @@ export default { attributes: { value: configValue[configKey] } }; - return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) + return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } }) }, toggleOpenGroup ({ state, dispatch }, group) { let open_groups = [ ...state.config.open_groups ]; -- GitLab