From ed99ea20cbaecbee56145a934f80e24e548ad0dc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 30 Jun 2023 11:07:51 -0400 Subject: [PATCH] feat: allow FormData object to be passed in to the API module Currently, only objects can be passed in, and it is automatically serialized into json and sent via jQuery .ajax(). This PR extends the module so a FormData object can be passed in, and updates the module so it uses Fetch API instead of jQuery. At this time regular requests continue to use jQuery for backwards compatibility. Use case: file uploads via API. --- public/src/modules/api.js | 82 ++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/public/src/modules/api.js b/public/src/modules/api.js index ed0c7c2d2b..4266b70e18 100644 --- a/public/src/modules/api.js +++ b/public/src/modules/api.js @@ -37,27 +37,60 @@ function call(options, callback) { } async function xhr(options, cb) { - // Allow options to be modified by plugins, etc. - ({ options } = await fireHook('filter:api.options', { options })); - - $.ajax(options) - .done((res) => { - cb(null, ( - res && - res.hasOwnProperty('status') && - res.hasOwnProperty('response') ? res.response : (res || {}) - )); - }) - .fail((ev) => { - let errMessage; - if (ev.responseJSON) { - errMessage = ev.responseJSON.status && ev.responseJSON.status.message ? - ev.responseJSON.status.message : - ev.responseJSON.error; - } + /** + * N.B. fetch api is only used when payload is a FormData object! + * + * This is because the passed-in options are different between fetch/jQuery .ajax() + * If we updated the code to use only fetch, it would be a breaking change. + * + * Prior to v3.3 there was no support for sending in FormData, so the addition of fetch + * handling is not breaking. + * + * Break this for v4 by making everything use fetch api. + */ + + // Adjust options based on payload type + if (options.payload instanceof FormData) { + const url = options.url; + options.body = options.payload; + delete options.payload; + delete options.url; + + // Allow options to be modified by plugins, etc. + ({ options } = await fireHook('filter:api.fetchOptions', { options })); + + await fetch(url, { + ...options, + }).then((res) => { + cb(null, res); + }).catch(cb); + } else { + options.data = JSON.stringify(options.payload || {}); + options.contentType = 'application/json; charset=utf-8'; + delete options.payload; + + // Allow options to be modified by plugins, etc. + ({ options } = await fireHook('filter:api.options', { options })); + + $.ajax(options) + .done((res) => { + cb(null, ( + res && + res.hasOwnProperty('status') && + res.hasOwnProperty('response') ? res.response : (res || {}) + )); + }) + .fail((ev) => { + let errMessage; + if (ev.responseJSON) { + errMessage = ev.responseJSON.status && ev.responseJSON.status.message ? + ev.responseJSON.status.message : + ev.responseJSON.error; + } - cb(new Error(errMessage || ev.statusText)); - }); + cb(new Error(errMessage || ev.statusText)); + }); + } } export function get(route, payload, onSuccess) { @@ -77,8 +110,7 @@ export function post(route, payload, onSuccess) { return call({ url: route, method: 'post', - data: JSON.stringify(payload || {}), - contentType: 'application/json; charset=utf-8', + payload, headers: { 'x-csrf-token': config.csrf_token, }, @@ -89,8 +121,7 @@ export function patch(route, payload, onSuccess) { return call({ url: route, method: 'patch', - data: JSON.stringify(payload || {}), - contentType: 'application/json; charset=utf-8', + payload, headers: { 'x-csrf-token': config.csrf_token, }, @@ -101,8 +132,7 @@ export function put(route, payload, onSuccess) { return call({ url: route, method: 'put', - data: JSON.stringify(payload || {}), - contentType: 'application/json; charset=utf-8', + payload, headers: { 'x-csrf-token': config.csrf_token, },