mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
commit
653b470fec
5 changed files with 68 additions and 12 deletions
18
CHANGELOG.md
18
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).
|
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
|
## [3.8.1] - 2022-12-06
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
import { Fetch } from '../../utils/types';
|
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 {
|
export class HttpClient {
|
||||||
constructor(private readonly fetch: Fetch) {}
|
constructor(private readonly fetch: Fetch) {}
|
||||||
|
|
||||||
public readonly fetchJson = <T>(url: string, options?: RequestInit): Promise<T> =>
|
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();
|
const json = await resp.json();
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
@ -15,7 +32,7 @@ export class HttpClient {
|
||||||
});
|
});
|
||||||
|
|
||||||
public readonly fetchEmpty = (url: string, options?: RequestInit): Promise<void> =>
|
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) {
|
if (!resp.ok) {
|
||||||
throw await resp.json();
|
throw await resp.json();
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
|
||||||
getVisits={loadVisits}
|
getVisits={loadVisits}
|
||||||
cancelGetVisits={cancelGetShortUrlVisits}
|
cancelGetVisits={cancelGetShortUrlVisits}
|
||||||
visitsInfo={shortUrlVisits}
|
visitsInfo={shortUrlVisits}
|
||||||
domain={domain}
|
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Button, Progress, Row } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
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 classNames from 'classnames';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import { Message } from '../utils/Message';
|
import { Message } from '../utils/Message';
|
||||||
|
@ -35,7 +35,6 @@ export type VisitsStatsProps = PropsWithChildren<{
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
cancelGetVisits: () => void;
|
cancelGetVisits: () => void;
|
||||||
domain?: string;
|
|
||||||
exportCsv: (visits: NormalizedVisit[]) => void;
|
exportCsv: (visits: NormalizedVisit[]) => void;
|
||||||
isOrphanVisits?: boolean;
|
isOrphanVisits?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
@ -62,7 +61,6 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
visitsInfo,
|
visitsInfo,
|
||||||
getVisits,
|
getVisits,
|
||||||
cancelGetVisits,
|
cancelGetVisits,
|
||||||
domain,
|
|
||||||
settings,
|
settings,
|
||||||
exportCsv,
|
exportCsv,
|
||||||
selectedServer,
|
selectedServer,
|
||||||
|
@ -86,11 +84,9 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
|
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
|
||||||
const botsSupported = supportsBotVisits(selectedServer);
|
const botsSupported = supportsBotVisits(selectedServer);
|
||||||
const isFirstLoad = useRef(true);
|
const isFirstLoad = useRef(true);
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
const buildSectionUrl = (subPath?: string) => {
|
const buildSectionUrl = (subPath?: string) => (!subPath ? search : `${subPath}${search}`);
|
||||||
const query = domain ? `?domain=${domain}` : '';
|
|
||||||
return !subPath ? `${query}` : `${subPath}${query}`;
|
|
||||||
};
|
|
||||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [visits]);
|
const normalizedVisits = useMemo(() => normalizeVisits(visits), [visits]);
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
||||||
() => processStatsFromVisits(normalizedVisits),
|
() => processStatsFromVisits(normalizedVisits),
|
||||||
|
|
|
@ -14,13 +14,39 @@ describe('HttpClient', () => {
|
||||||
await expect(httpClient.fetchJson('')).rejects.toEqual(theError);
|
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' };
|
const theJson = { foo: 'bar' };
|
||||||
fetch.mockResolvedValue({ json: () => theJson, ok: true });
|
fetch.mockResolvedValue({ json: () => theJson, ok: true });
|
||||||
|
|
||||||
const result = await httpClient.fetchJson('');
|
const result = await httpClient.fetchJson('the_url', options);
|
||||||
|
|
||||||
expect(result).toEqual(theJson);
|
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' },
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue