Use /tags/stats endpoint when the server supports it

This commit is contained in:
Alejandro Celaya 2023-03-18 16:26:28 +01:00
parent ddaec7c6ac
commit b87b108e53
5 changed files with 40 additions and 8 deletions

View file

@ -16,6 +16,7 @@ import type {
ShlinkShortUrlsResponse,
ShlinkTags,
ShlinkTagsResponse,
ShlinkTagsStatsResponse,
ShlinkVisits,
ShlinkVisitsOverview,
ShlinkVisitsParams,
@ -85,10 +86,14 @@ export class ShlinkApiClient {
): Promise<ShortUrl> =>
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit);
public readonly listTags = async (): Promise<ShlinkTags> =>
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
.then(({ tags }) => tags)
.then(({ data, stats }) => ({ tags: data, stats }));
public readonly listTags = async (useTagsStatsEndpoint: boolean): Promise<ShlinkTags> =>
(useTagsStatsEndpoint
? this.performRequest<{ tags: ShlinkTagsStatsResponse }>('/tags/stats', 'GET')
.then(({ tags }) => tags)
.then(({ data }) => ({ tags: data.map(({ tag }) => tag), stats: data }))
: this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
.then(({ tags }) => tags)
.then(({ data, stats }) => ({ tags: data, stats })));
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags }));

View file

@ -31,9 +31,14 @@ export interface ShlinkTags {
export interface ShlinkTagsResponse {
data: string[];
/** @deprecated Present only when withStats=true is provided, which is deprecated */
stats: ShlinkTagsStats[];
}
export interface ShlinkTagsStatsResponse {
data: ShlinkTagsStats[];
}
export interface ShlinkPaginator {
currentPage: number;
pagesCount: number;

View file

@ -5,6 +5,7 @@ import type { ShlinkTags } from '../../api/types';
import type { ProblemDetailsError } from '../../api/types/errors';
import { parseApiError } from '../../api/utils';
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
import { supportedFeatures } from '../../utils/helpers/features';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { createNewVisits } from '../../visits/reducers/visitCreation';
import type { CreateVisit, Stats } from '../../visits/types';
@ -70,14 +71,14 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => createAsyncThunk(
`${REDUCER_PREFIX}/listTags`,
async (_: void, { getState }): Promise<ListTags> => {
const { tagsList } = getState();
const { tagsList, selectedServer } = getState();
if (!force && !isEmpty(tagsList.tags)) {
return tagsList;
}
const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
const { tags, stats = [] }: ShlinkTags = await shlinkListTags(supportedFeatures.tagsStats(selectedServer));
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
acc[tag] = { shortUrlsCount, visitsCount };

View file

@ -14,11 +14,12 @@ export const supportedFeatures = {
defaultDomainRedirectsEdition: matchesMinVersion('2.10.0'),
nonOrphanVisits: matchesMinVersion('3.0.0'),
allTagsFiltering: matchesMinVersion('3.0.0'),
tagsStats: matchesMinVersion('3.0.0'),
domainVisits: matchesMinVersion('3.1.0'),
excludeBotsOnShortUrls: matchesMinVersion('3.4.0'),
filterDisabledUrls: matchesMinVersion('3.4.0'),
deviceLongUrls: matchesMinVersion('3.5.0'),
} as const;
} as const satisfies Record<string, ReturnType<typeof matchesMinVersion>>;
Object.freeze(supportedFeatures);

View file

@ -202,7 +202,7 @@ describe('ShlinkApiClient', () => {
});
const { listTags } = buildApiClient();
const result = await listTags();
const result = await listTags(false);
expect({ tags: expectedTags }).toEqual(result);
expect(fetchJson).toHaveBeenCalledWith(
@ -210,6 +210,26 @@ describe('ShlinkApiClient', () => {
expect.objectContaining({ method: 'GET' }),
);
});
it('can use /tags/stats endpoint', async () => {
const expectedTags = ['foo', 'bar'];
const expectedStats = expectedTags.map((tag) => ({ tag, shortUrlsCount: 10, visitsCount: 10 }));
fetchJson.mockResolvedValue({
tags: {
data: expectedStats,
},
});
const { listTags } = buildApiClient();
const result = await listTags(true);
expect({ tags: expectedTags, stats: expectedStats }).toEqual(result);
expect(fetchJson).toHaveBeenCalledWith(
expect.stringContaining('/tags/stats'),
expect.objectContaining({ method: 'GET' }),
);
});
});
describe('deleteTags', () => {