import axios, { AxiosInstance } from 'axios'; import { store } from '@/state'; import { Model } from '@/api/definitions'; const http: AxiosInstance = axios.create({ timeout: 20000, headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json', 'Content-Type': 'application/json', }, }); http.interceptors.request.use(req => { const cookies = document.cookie.split(';').reduce((obj, val) => { const [ key, value ] = val.trim().split('=').map(decodeURIComponent); return { ...obj, [key]: value }; }, {} as Record); req.headers['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN'] || 'nil'; return req; }); http.interceptors.request.use(req => { if (!req.url?.endsWith('/resources')) { store.getActions().progress.startContinuous(); } return req; }); http.interceptors.response.use(resp => { if (!resp.request?.url?.endsWith('/resources')) { store.getActions().progress.setComplete(); } return resp; }, error => { store.getActions().progress.setComplete(); throw error; }); export default http; /** * Converts an error into a human readable response. Mostly just a generic helper to * make sure we display the message from the server back to the user if we can. */ export function httpErrorToHuman (error: any): string { if (error.response && error.response.data) { let { data } = error.response; // Some non-JSON requests can still return the error as a JSON block. In those cases, attempt // to parse it into JSON so we can display an actual error. if (typeof data === 'string') { try { data = JSON.parse(data); } catch (e) { // do nothing, bad json } } if (data.errors && data.errors[0] && data.errors[0].detail) { return data.errors[0].detail; } // Errors from wings directory, mostly just for file uploads. if (data.error && typeof data.error === 'string') { return data.error; } } return error.message; } export interface FractalResponseData { object: string; attributes: { [k: string]: any; relationships?: Record; }; } export interface FractalResponseList { object: 'list'; data: FractalResponseData[]; } export interface FractalPaginatedResponse extends FractalResponseList { meta: { pagination: { total: number; count: number; /* eslint-disable camelcase */ per_page: number; current_page: number; total_pages: number; /* eslint-enable camelcase */ }; }; } export interface PaginatedResult { items: T[]; pagination: PaginationDataSet; } export interface PaginationDataSet { total: number; count: number; perPage: number; currentPage: number; totalPages: number; } export function getPaginationSet (data: any): PaginationDataSet { return { total: data.total, count: data.count, perPage: data.per_page, currentPage: data.current_page, totalPages: data.total_pages, }; } type QueryBuilderFilterValue = string | number | boolean | null; export interface QueryBuilderParams { filters?: { [K in FilterKeys]?: QueryBuilderFilterValue | Readonly; }; sorts?: { [K in SortKeys]?: -1 | 0 | 1 | 'asc' | 'desc' | null; }; } /** * Helper function that parses a data object provided and builds query parameters * for the Laravel Query Builder package automatically. This will apply sorts and * filters deterministically based on the provided values. */ export const withQueryBuilderParams = (data?: QueryBuilderParams): Record => { if (!data) return {}; const filters = Object.keys(data.filters || {}).reduce((obj, key) => { const value = data.filters?.[key]; return !value || value === '' ? obj : { ...obj, [`filter[${key}]`]: value }; }, {} as NonNullable); const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => { const value = data.sorts?.[key]; if (!value || ![ 'asc', 'desc', 1, -1 ].includes(value)) { return arr; } return [ ...arr, (value === -1 || value === 'desc' ? '-' : '') + key ]; }, [] as string[]); return { ...filters, sorts: !sorts.length ? undefined : sorts.join(','), }; }; type TransformerFunc = (data: FractalResponseData) => Model; export const toPaginatedSet = ( response: FractalPaginatedResponse, transformer: T, ): PaginatedResult> => ({ items: response.data.map(transformer) as ReturnType[], pagination: getPaginationSet(response.meta.pagination), });