diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index f264d580..b7c78ba0 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -63,6 +63,7 @@ export default class ShlinkApiClient { this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }) .then(() => {}); + /* @deprecated. If using Shlink 2.6.0 or greater, use updateShortUrlMeta instead */ public readonly updateShortUrlTags = async ( shortCode: string, domain: OptionalString, @@ -75,9 +76,9 @@ export default class ShlinkApiClient { shortCode: string, domain: OptionalString, meta: ShlinkShortUrlMeta, - ): Promise => - this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta) - .then(() => meta); + ): Promise => + this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta) + .then(({ data }) => data); public readonly listTags = async (): Promise => this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 4ca5f7e3..b8383a74 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -59,6 +59,7 @@ export interface ShlinkVisitsParams { export interface ShlinkShortUrlMeta extends ShortUrlMeta { longUrl?: string; + tags?: string[]; } export interface ShlinkDomain { diff --git a/src/short-urls/reducers/shortUrlTags.ts b/src/short-urls/reducers/shortUrlTags.ts index 277386b2..0efc999f 100644 --- a/src/short-urls/reducers/shortUrlTags.ts +++ b/src/short-urls/reducers/shortUrlTags.ts @@ -1,4 +1,5 @@ import { Action, Dispatch } from 'redux'; +import { prop } from 'ramda'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; import { OptionalString } from '../../utils/utils'; @@ -6,6 +7,8 @@ import { ShortUrlIdentifier } from '../data'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ProblemDetailsError } from '../../api/types'; import { parseApiError } from '../../api/utils'; +import { isReachableServer } from '../../servers/data'; +import { versionMatch } from '../../utils/helpers/version'; /* eslint-disable padding-line-between-statements */ export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START'; @@ -50,10 +53,19 @@ export const editShortUrlTags = (buildShlinkApiClient: ShlinkApiClientBuilder) = tags: string[], ) => async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: EDIT_SHORT_URL_TAGS_START }); - const { updateShortUrlTags } = buildShlinkApiClient(getState); + const { selectedServer } = getState(); + const supportsTagsInPatch = isReachableServer(selectedServer) && versionMatch( + selectedServer.version, + { minVersion: '2.6.0' }, + ); + const { updateShortUrlTags, updateShortUrlMeta } = buildShlinkApiClient(getState); try { - const normalizedTags = await updateShortUrlTags(shortCode, domain, tags); + const normalizedTags = await ( + supportsTagsInPatch + ? updateShortUrlMeta(shortCode, domain, { tags }).then(prop('tags')) + : updateShortUrlTags(shortCode, domain, tags) + ); dispatch({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED }); } catch (e) { diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index 93a2d7dd..2c5d0ae7 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -3,6 +3,7 @@ import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; import { OptionalString } from '../../../src/utils/utils'; import { Mock } from 'ts-mockery'; import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('ShlinkApiClient', () => { const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance; @@ -139,16 +140,17 @@ describe('ShlinkApiClient', () => { describe('updateShortUrlMeta', () => { it.each(shortCodesWithDomainCombinations)('properly updates short URL meta', async (shortCode, domain) => { - const expectedMeta = { + const meta = { maxVisits: 50, validSince: '2025-01-01T10:00:00+01:00', }; - const axiosSpy = createAxiosMock(); + const expectedResp = Mock.of() + const axiosSpy = createAxiosMock({ data: expectedResp }); const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy, '', ''); - const result = await updateShortUrlMeta(shortCode, domain, expectedMeta); + const result = await updateShortUrlMeta(shortCode, domain, meta); - expect(expectedMeta).toEqual(result); + expect(expectedResp).toEqual(result); expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({ url: `/short-urls/${shortCode}`, method: 'PATCH', diff --git a/test/short-urls/helpers/EditTagsModal.test.tsx b/test/short-urls/helpers/EditTagsModal.test.tsx index fddccad0..4c7294b3 100644 --- a/test/short-urls/helpers/EditTagsModal.test.tsx +++ b/test/short-urls/helpers/EditTagsModal.test.tsx @@ -1,10 +1,10 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; +import { Modal } from 'reactstrap'; import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal'; import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags'; import { OptionalString } from '../../../src/utils/utils'; -import { Modal } from 'reactstrap'; describe('', () => { let wrapper: ShallowWrapper; diff --git a/test/short-urls/reducers/shortUrlTags.test.ts b/test/short-urls/reducers/shortUrlTags.test.ts index 8542ac2d..004093bb 100644 --- a/test/short-urls/reducers/shortUrlTags.test.ts +++ b/test/short-urls/reducers/shortUrlTags.test.ts @@ -9,6 +9,7 @@ import reducer, { EditShortUrlTagsAction, } from '../../../src/short-urls/reducers/shortUrlTags'; import { ShlinkState } from '../../../src/container/types'; +import { ReachableServer, SelectedServer } from '../../../src/servers/data'; describe('shortUrlTagsReducer', () => { const tags = [ 'foo', 'bar', 'baz' ]; @@ -60,9 +61,10 @@ describe('shortUrlTagsReducer', () => { describe('editShortUrlTags', () => { const updateShortUrlTags = jest.fn(); - const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrlTags }); + const updateShortUrlMeta = jest.fn(); + const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrlTags, updateShortUrlMeta }); const dispatch = jest.fn(); - const getState = () => Mock.all(); + const buildGetState = (selectedServer?: SelectedServer) => () => Mock.of({ selectedServer }); afterEach(jest.clearAllMocks); @@ -71,11 +73,12 @@ describe('shortUrlTagsReducer', () => { updateShortUrlTags.mockResolvedValue(normalizedTags); - await editShortUrlTags(buildShlinkApiClient)(shortCode, domain, tags)(dispatch, getState); + await editShortUrlTags(buildShlinkApiClient)(shortCode, domain, tags)(dispatch, buildGetState()); expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); expect(updateShortUrlTags).toHaveBeenCalledTimes(1); expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, domain, tags); + expect(updateShortUrlMeta).not.toHaveBeenCalled(); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START }); expect(dispatch).toHaveBeenNthCalledWith( @@ -84,13 +87,35 @@ describe('shortUrlTagsReducer', () => { ); }); + it('calls updateShortUrlMeta when server is version 2.6.0 or above', async () => { + const normalizedTags = [ 'bar', 'foo' ]; + + updateShortUrlMeta.mockResolvedValue({ tags: normalizedTags }); + + await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)( + dispatch, + buildGetState(Mock.of({ printableVersion: '', version: '2.6.0' })), + ); + + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, undefined, { tags }); + expect(updateShortUrlTags).not.toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START }); + expect(dispatch).toHaveBeenNthCalledWith( + 2, + { type: SHORT_URL_TAGS_EDITED, tags: normalizedTags, shortCode }, + ); + }); + it('dispatches error on failure', async () => { const error = new Error(); updateShortUrlTags.mockRejectedValue(error); try { - await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)(dispatch, getState); + await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)(dispatch, buildGetState()); } catch (e) { expect(e).toBe(error); } diff --git a/test/visits/helpers/MapModal.test.tsx b/test/visits/helpers/MapModal.test.tsx index 71cdda26..8bab0a3a 100644 --- a/test/visits/helpers/MapModal.test.tsx +++ b/test/visits/helpers/MapModal.test.tsx @@ -1,8 +1,8 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Marker, Popup } from 'react-leaflet'; +import { Modal } from 'reactstrap'; import MapModal from '../../../src/visits/helpers/MapModal'; import { CityStats } from '../../../src/visits/types'; -import { Modal } from 'reactstrap'; describe('', () => { let wrapper: ShallowWrapper;