Merge pull request #768 from shlinkio/develop

Release 3.8.2
This commit is contained in:
Alejandro Celaya 2022-12-17 10:07:37 +01:00 committed by GitHub
commit cc88f7678c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 452 additions and 253 deletions

View file

@ -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*

625
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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 = <T>(url: string, options?: RequestInit): Promise<T> =>
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<void> =>
this.fetch(url, options).then(async (resp) => {
this.fetch(url, withJsonContentType(options)).then(async (resp) => {
if (!resp.ok) {
throw await resp.json();
}

View file

@ -55,7 +55,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
getVisits={loadVisits}
cancelGetVisits={cancelGetShortUrlVisits}
visitsInfo={shortUrlVisits}
domain={domain}
settings={settings}
exportCsv={exportCsv}
selectedServer={selectedServer}

View file

@ -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<VisitsStatsProps> = ({
visitsInfo,
getVisits,
cancelGetVisits,
domain,
settings,
exportCsv,
selectedServer,
@ -86,11 +84,9 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
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),

View file

@ -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' },
}));
});
});