Merge pull request #7216 from tchak/refactor-use-http-request
Use fetch everywhere
This commit is contained in:
commit
1126099e0b
5 changed files with 153 additions and 93 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { getJSON, ajax, fire } from '@utils';
|
import { httpRequest, fire } from '@utils';
|
||||||
import type { Feature, FeatureCollection, Geometry } from 'geojson';
|
import type { Feature, FeatureCollection, Geometry } from 'geojson';
|
||||||
|
|
||||||
export const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
export const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
||||||
|
@ -37,7 +37,9 @@ export function useFeatureCollection(
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: callback(features)
|
features: callback(features)
|
||||||
}));
|
}));
|
||||||
ajax({ url, type: 'GET' }).catch(() => null);
|
httpRequest(url)
|
||||||
|
.js()
|
||||||
|
.catch(() => null);
|
||||||
},
|
},
|
||||||
[url, setFeatureCollection]
|
[url, setFeatureCollection]
|
||||||
);
|
);
|
||||||
|
@ -99,7 +101,10 @@ export function useFeatureCollection(
|
||||||
try {
|
try {
|
||||||
const newFeatures: Feature[] = [];
|
const newFeatures: Feature[] = [];
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const data = await getJSON(url, { feature, source }, 'post');
|
const data = await httpRequest(url, {
|
||||||
|
method: 'post',
|
||||||
|
json: { feature, source }
|
||||||
|
}).json<{ feature: Feature & { lid?: string | number } }>();
|
||||||
if (data) {
|
if (data) {
|
||||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||||
data.feature.lid = feature.id;
|
data.feature.lid = feature.id;
|
||||||
|
@ -128,9 +133,15 @@ export function useFeatureCollection(
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const id = feature.properties?.id;
|
const id = feature.properties?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
await getJSON(`${url}/${id}`, { feature }, 'patch');
|
await httpRequest(`${url}/${id}`, {
|
||||||
|
method: 'patch',
|
||||||
|
json: { feature }
|
||||||
|
}).json();
|
||||||
} else {
|
} else {
|
||||||
const data = await getJSON(url, { feature, source }, 'post');
|
const data = await httpRequest(url, {
|
||||||
|
method: 'post',
|
||||||
|
json: { feature, source }
|
||||||
|
}).json<{ feature: Feature & { lid?: string | number } }>();
|
||||||
if (data) {
|
if (data) {
|
||||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||||
data.feature.lid = feature.id;
|
data.feature.lid = feature.id;
|
||||||
|
@ -157,7 +168,7 @@ export function useFeatureCollection(
|
||||||
const deletedFeatures = [];
|
const deletedFeatures = [];
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
const id = feature.properties?.id;
|
const id = feature.properties?.id;
|
||||||
await getJSON(`${url}/${id}`, null, 'delete');
|
await httpRequest(`${url}/${id}`, { method: 'delete' }).json();
|
||||||
deletedFeatures.push(feature);
|
deletedFeatures.push(feature);
|
||||||
}
|
}
|
||||||
removeFeatures(deletedFeatures, external);
|
removeFeatures(deletedFeatures, external);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getJSON } from '@utils';
|
import { httpRequest } from '@utils';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
type Operation = {
|
type Operation = {
|
||||||
|
@ -45,7 +45,7 @@ export class OperationsQueue {
|
||||||
const url = `${this.baseUrl}${path}`;
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getJSON(url, payload, method);
|
const data = await httpRequest(url, { method, json: payload }).json();
|
||||||
resolve(data);
|
resolve(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e as OperationError, reject);
|
handleError(e as OperationError, reject);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { QueryClient, QueryFunction } from 'react-query';
|
import { QueryClient, QueryFunction } from 'react-query';
|
||||||
import { getJSON, isNumeric } from '@utils';
|
import { httpRequest, isNumeric } from '@utils';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
|
|
||||||
type Gon = {
|
type Gon = {
|
||||||
|
@ -57,39 +57,27 @@ function buildURL(scope: string, term: string, extra?: string) {
|
||||||
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
|
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOptions(): [RequestInit, AbortController | null] {
|
|
||||||
if (window.AbortController) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const signal = controller.signal;
|
|
||||||
return [{ signal }, controller];
|
|
||||||
}
|
|
||||||
return [{}, null];
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
|
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
|
||||||
queryKey: [scope, term, extra]
|
queryKey: [scope, term, extra],
|
||||||
|
signal
|
||||||
}) => {
|
}) => {
|
||||||
if (scope == 'pays') {
|
if (scope == 'pays') {
|
||||||
return matchSorter(await getPays(), term, { keys: ['label'] });
|
return matchSorter(await getPays(signal), term, { keys: ['label'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildURL(scope, term, extra);
|
const url = buildURL(scope, term, extra);
|
||||||
const [options, controller] = buildOptions();
|
return httpRequest(url, { csrf: false, signal }).json();
|
||||||
const promise = fetch(url, options).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw new Error(`Error fetching from "${scope}" API`);
|
|
||||||
});
|
|
||||||
return Object.assign(promise, {
|
|
||||||
cancel: () => controller && controller.abort()
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let paysCache: { label: string }[];
|
let paysCache: { label: string }[];
|
||||||
async function getPays(): Promise<{ label: string }[]> {
|
async function getPays(signal?: AbortSignal): Promise<{ label: string }[]> {
|
||||||
if (!paysCache) {
|
if (!paysCache) {
|
||||||
paysCache = await getJSON('/api/pays', null);
|
const data = await httpRequest('/api/pays', { signal }).json<
|
||||||
|
typeof paysCache
|
||||||
|
>();
|
||||||
|
if (data) {
|
||||||
|
paysCache = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return paysCache;
|
return paysCache;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ajax, delegate } from '@utils';
|
import { httpRequest, delegate } from '@utils';
|
||||||
|
|
||||||
addEventListener('DOMContentLoaded', () => {
|
addEventListener('DOMContentLoaded', () => {
|
||||||
attachementPoller.deactivate();
|
attachementPoller.deactivate();
|
||||||
|
@ -69,7 +69,9 @@ class RemotePoller {
|
||||||
for (let url of urls) {
|
for (let url of urls) {
|
||||||
// Start the request. The JS payload in the response will update the page.
|
// Start the request. The JS payload in the response will update the page.
|
||||||
// (Errors are ignored, because background tasks shouldn't report errors to the user.)
|
// (Errors are ignored, because background tasks shouldn't report errors to the user.)
|
||||||
ajax({ url, type: 'get' }).catch(() => {});
|
httpRequest(url)
|
||||||
|
.js()
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import debounce from 'debounce';
|
import debounce from 'debounce';
|
||||||
|
import { session } from '@hotwired/turbo';
|
||||||
|
|
||||||
export { debounce };
|
export { debounce };
|
||||||
export const { fire, csrfToken } = Rails;
|
export const { fire, csrfToken, cspNonce } = Rails;
|
||||||
|
|
||||||
export function show(el: HTMLElement) {
|
export function show(el: HTMLElement) {
|
||||||
el && el.classList.remove('hidden');
|
el && el.classList.remove('hidden');
|
||||||
|
@ -93,7 +94,7 @@ export function ajax(options: Rails.AjaxOptions) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
response: Response;
|
response: Response;
|
||||||
|
|
||||||
constructor(response: Response) {
|
constructor(response: Response) {
|
||||||
|
@ -102,18 +103,126 @@ class ResponseError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getJSON(url: string, data: unknown, method = 'GET') {
|
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
||||||
const { query, ...options } = fetchOptions(data, method);
|
|
||||||
|
|
||||||
return fetch(`${url}${query}`, options).then((response) => {
|
// Perform an HTTP request using `fetch` API,
|
||||||
if (response.ok) {
|
// and handle the result depending on the MIME type.
|
||||||
if (response.status === 204) {
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// Execute a GET request, and return the response as parsed JSON
|
||||||
|
// const parsedJson = await httpRequest(url).json();
|
||||||
|
//
|
||||||
|
// Execute a POST request with some JSON payload
|
||||||
|
// const parsedJson = await httpRequest(url, { method: 'POST', json: '{ "foo": 1 }').json();
|
||||||
|
//
|
||||||
|
// Execute a GET request, and apply the Turbo stream in the Response
|
||||||
|
// await httpRequest(url).turbo();
|
||||||
|
//
|
||||||
|
// Execute a GET request, and interpret the JavaScript code in the Response
|
||||||
|
// DEPRECATED: Don't use this in new code; instead let the server respond with a turbo stream
|
||||||
|
// await httpRequest(url).js();
|
||||||
|
//
|
||||||
|
export function httpRequest(
|
||||||
|
url: string,
|
||||||
|
{
|
||||||
|
csrf = true,
|
||||||
|
timeout = FETCH_TIMEOUT,
|
||||||
|
json,
|
||||||
|
controller,
|
||||||
|
...init
|
||||||
|
}: RequestInit & {
|
||||||
|
csrf?: boolean;
|
||||||
|
json?: unknown;
|
||||||
|
timeout?: number | false;
|
||||||
|
controller?: AbortController;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const headers = new Headers(init.headers);
|
||||||
|
if (csrf) {
|
||||||
|
headers.set('x-csrf-token', csrfToken() ?? '');
|
||||||
|
headers.set('x-requested-with', 'XMLHttpRequest');
|
||||||
|
init.credentials = 'same-origin';
|
||||||
|
}
|
||||||
|
init.headers = headers;
|
||||||
|
init.method = init.method?.toUpperCase() ?? 'GET';
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
headers.set('content-type', 'application/json');
|
||||||
|
init.body = JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: number;
|
||||||
|
if (!init.signal) {
|
||||||
|
controller = createAbortController(controller);
|
||||||
|
if (controller) {
|
||||||
|
init.signal = controller.signal;
|
||||||
|
if (timeout != false) {
|
||||||
|
timer = setTimeout(() => controller?.abort(), timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (init: RequestInit, accept?: string): Promise<Response> => {
|
||||||
|
if (accept && init.headers instanceof Headers) {
|
||||||
|
init.headers.set('accept', accept);
|
||||||
|
}
|
||||||
|
return fetch(url, init)
|
||||||
|
.then((response) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response;
|
||||||
|
} else if (response.status == 401) {
|
||||||
|
location.reload(); // reload whole page so Devise will redirect to sign-in
|
||||||
|
}
|
||||||
|
throw new ResponseError(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
async json<T>(): Promise<T | null> {
|
||||||
|
const response = await request(init, 'application/json');
|
||||||
|
if (response.status == 204) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
|
},
|
||||||
|
async turbo(): Promise<void> {
|
||||||
|
const response = await request(init, 'text/vnd.turbo-stream.html');
|
||||||
|
if (response.status != 204) {
|
||||||
|
const stream = await response.text();
|
||||||
|
session.renderStreamMessage(stream);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async js(): Promise<void> {
|
||||||
|
const response = await request(init, 'text/javascript');
|
||||||
|
if (response.status != 204) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
const nonce = cspNonce();
|
||||||
|
if (nonce) {
|
||||||
|
script.setAttribute('nonce', nonce);
|
||||||
|
}
|
||||||
|
script.text = await response.text();
|
||||||
|
document.head.appendChild(script);
|
||||||
|
document.head.removeChild(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new ResponseError(response);
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function createAbortController(controller?: AbortController) {
|
||||||
|
if (controller) {
|
||||||
|
return controller;
|
||||||
|
} else if (window.AbortController) {
|
||||||
|
return new AbortController();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
||||||
|
@ -162,53 +271,3 @@ export function timeoutable<T>(
|
||||||
});
|
});
|
||||||
return Promise.race([promise, timeoutPromise]);
|
return Promise.race([promise, timeoutPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
|
||||||
|
|
||||||
function fetchOptions(data: unknown, method = 'GET') {
|
|
||||||
const options: RequestInit & {
|
|
||||||
query: string;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
} = {
|
|
||||||
query: '',
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
headers: {
|
|
||||||
accept: 'application/json',
|
|
||||||
'x-csrf-token': csrfToken() ?? '',
|
|
||||||
'x-requested-with': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
if (options.method === 'GET') {
|
|
||||||
options.query = objectToQuerystring(data as Record<string, string>);
|
|
||||||
} else {
|
|
||||||
options.headers['content-type'] = 'application/json';
|
|
||||||
options.body = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.AbortController) {
|
|
||||||
const controller = new AbortController();
|
|
||||||
options.signal = controller.signal;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
}, FETCH_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectToQuerystring(obj: Record<string, string>): string {
|
|
||||||
return Object.keys(obj).reduce(function (query, key, i) {
|
|
||||||
return [
|
|
||||||
query,
|
|
||||||
i === 0 ? '?' : '&',
|
|
||||||
encodeURIComponent(key),
|
|
||||||
'=',
|
|
||||||
encodeURIComponent(obj[key])
|
|
||||||
].join('');
|
|
||||||
}, '');
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue