diff --git a/CHANGELOG.md b/CHANGELOG.md index a7309395..e2f0c3f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). +## [3.8.2] - 2022-12-17 +### Added +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* [#766](https://github.com/shlinkio/shlink-web-client/issues/766) Fixed visits query being lost when switching between sub-sections. +* [#765](https://github.com/shlinkio/shlink-web-client/issues/765) Added missing `"Content-Type": "application/json"` to requests with payload, making older Shlink versions fail. + + ## [3.8.1] - 2022-12-06 ### Added * *Nothing* diff --git a/src/common/services/HttpClient.ts b/src/common/services/HttpClient.ts index af8aedf3..9a07cfea 100644 --- a/src/common/services/HttpClient.ts +++ b/src/common/services/HttpClient.ts @@ -1,10 +1,27 @@ import { Fetch } from '../../utils/types'; +const applicationJsonHeader = { 'Content-Type': 'application/json' }; +const withJsonContentType = (options?: RequestInit): RequestInit | undefined => { + if (!options?.body) { + return options; + } + + return options ? { + ...options, + headers: { + ...(options.headers ?? {}), + ...applicationJsonHeader, + }, + } : { + headers: applicationJsonHeader, + }; +}; + export class HttpClient { constructor(private readonly fetch: Fetch) {} public readonly fetchJson = (url: string, options?: RequestInit): Promise => - this.fetch(url, options).then(async (resp) => { + this.fetch(url, withJsonContentType(options)).then(async (resp) => { const json = await resp.json(); if (!resp.ok) { @@ -15,7 +32,7 @@ export class HttpClient { }); public readonly fetchEmpty = (url: string, options?: RequestInit): Promise => - this.fetch(url, options).then(async (resp) => { + this.fetch(url, withJsonContentType(options)).then(async (resp) => { if (!resp.ok) { throw await resp.json(); } diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index 3e17ae46..0d94f87a 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -55,7 +55,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu getVisits={loadVisits} cancelGetVisits={cancelGetShortUrlVisits} visitsInfo={shortUrlVisits} - domain={domain} settings={settings} exportCsv={exportCsv} selectedServer={selectedServer} diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index ff3e74c2..639d8858 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -4,7 +4,7 @@ import { Button, Progress, Row } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons'; import { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import { Route, Routes, Navigate } from 'react-router-dom'; +import { Route, Routes, Navigate, useLocation } from 'react-router-dom'; import classNames from 'classnames'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import { Message } from '../utils/Message'; @@ -35,7 +35,6 @@ export type VisitsStatsProps = PropsWithChildren<{ settings: Settings; selectedServer: SelectedServer; cancelGetVisits: () => void; - domain?: string; exportCsv: (visits: NormalizedVisit[]) => void; isOrphanVisits?: boolean; }>; @@ -62,7 +61,6 @@ export const VisitsStats: FC = ({ visitsInfo, getVisits, cancelGetVisits, - domain, settings, exportCsv, selectedServer, @@ -86,11 +84,9 @@ export const VisitsStats: FC = ({ const [highlightedLabel, setHighlightedLabel] = useState(); const botsSupported = supportsBotVisits(selectedServer); const isFirstLoad = useRef(true); + const { search } = useLocation(); - const buildSectionUrl = (subPath?: string) => { - const query = domain ? `?domain=${domain}` : ''; - return !subPath ? `${query}` : `${subPath}${query}`; - }; + const buildSectionUrl = (subPath?: string) => (!subPath ? search : `${subPath}${search}`); const normalizedVisits = useMemo(() => normalizeVisits(visits), [visits]); const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo( () => processStatsFromVisits(normalizedVisits), diff --git a/test/common/services/HttpClient.test.ts b/test/common/services/HttpClient.test.ts index b894cd32..ea01faf5 100644 --- a/test/common/services/HttpClient.test.ts +++ b/test/common/services/HttpClient.test.ts @@ -14,13 +14,39 @@ describe('HttpClient', () => { await expect(httpClient.fetchJson('')).rejects.toEqual(theError); }); - it('return json on failure', async () => { + it.each([ + [undefined], + [{}], + [{ body: undefined }], + [{ body: '' }], + ])('return json on failure', async (options) => { const theJson = { foo: 'bar' }; fetch.mockResolvedValue({ json: () => theJson, ok: true }); - const result = await httpClient.fetchJson(''); + const result = await httpClient.fetchJson('the_url', options); expect(result).toEqual(theJson); + expect(fetch).toHaveBeenCalledWith('the_url', options); + }); + + it.each([ + [{ body: 'the_body' }], + [{ + body: 'the_body', + headers: { + 'Content-Type': 'text/plain', + }, + }], + ])('forwards JSON content-type when appropriate', async (options) => { + const theJson = { foo: 'bar' }; + fetch.mockResolvedValue({ json: () => theJson, ok: true }); + + const result = await httpClient.fetchJson('the_url', options); + + expect(result).toEqual(theJson); + expect(fetch).toHaveBeenCalledWith('the_url', expect.objectContaining({ + headers: { 'Content-Type': 'application/json' }, + })); }); });