mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 23:07:26 +03:00
Merge pull request #393 from acelaya-forks/feature/patch-tags
Feature/patch tags
This commit is contained in:
commit
46d012b6ff
8 changed files with 57 additions and 16 deletions
|
@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
* [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* *Nothing*
|
* [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -63,6 +63,7 @@ export default class ShlinkApiClient {
|
||||||
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
|
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
|
||||||
.then(() => {});
|
.then(() => {});
|
||||||
|
|
||||||
|
/* @deprecated. If using Shlink 2.6.0 or greater, use updateShortUrlMeta instead */
|
||||||
public readonly updateShortUrlTags = async (
|
public readonly updateShortUrlTags = async (
|
||||||
shortCode: string,
|
shortCode: string,
|
||||||
domain: OptionalString,
|
domain: OptionalString,
|
||||||
|
@ -75,9 +76,9 @@ export default class ShlinkApiClient {
|
||||||
shortCode: string,
|
shortCode: string,
|
||||||
domain: OptionalString,
|
domain: OptionalString,
|
||||||
meta: ShlinkShortUrlMeta,
|
meta: ShlinkShortUrlMeta,
|
||||||
): Promise<ShlinkShortUrlMeta> =>
|
): Promise<ShortUrl> =>
|
||||||
this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
|
||||||
.then(() => meta);
|
.then(({ data }) => data);
|
||||||
|
|
||||||
public readonly listTags = async (): Promise<ShlinkTags> =>
|
public readonly listTags = async (): Promise<ShlinkTags> =>
|
||||||
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
|
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
|
||||||
|
|
|
@ -59,6 +59,7 @@ export interface ShlinkVisitsParams {
|
||||||
|
|
||||||
export interface ShlinkShortUrlMeta extends ShortUrlMeta {
|
export interface ShlinkShortUrlMeta extends ShortUrlMeta {
|
||||||
longUrl?: string;
|
longUrl?: string;
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkDomain {
|
export interface ShlinkDomain {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { prop } from 'ramda';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
|
@ -6,6 +7,8 @@ import { ShortUrlIdentifier } from '../data';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { ProblemDetailsError } from '../../api/types';
|
import { ProblemDetailsError } from '../../api/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
|
import { isReachableServer } from '../../servers/data';
|
||||||
|
import { versionMatch } from '../../utils/helpers/version';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
|
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[],
|
tags: string[],
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
|
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 {
|
try {
|
||||||
const normalizedTags = await updateShortUrlTags(shortCode, domain, tags);
|
const normalizedTags = await (
|
||||||
|
supportsTagsInPatch
|
||||||
|
? updateShortUrlMeta(shortCode, domain, { tags }).then(prop('tags'))
|
||||||
|
: updateShortUrlTags(shortCode, domain, tags)
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<EditShortUrlTagsAction>({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED });
|
dispatch<EditShortUrlTagsAction>({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
|
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('ShlinkApiClient', () => {
|
describe('ShlinkApiClient', () => {
|
||||||
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
||||||
|
@ -139,16 +140,17 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('updateShortUrlMeta', () => {
|
describe('updateShortUrlMeta', () => {
|
||||||
it.each(shortCodesWithDomainCombinations)('properly updates short URL meta', async (shortCode, domain) => {
|
it.each(shortCodesWithDomainCombinations)('properly updates short URL meta', async (shortCode, domain) => {
|
||||||
const expectedMeta = {
|
const meta = {
|
||||||
maxVisits: 50,
|
maxVisits: 50,
|
||||||
validSince: '2025-01-01T10:00:00+01:00',
|
validSince: '2025-01-01T10:00:00+01:00',
|
||||||
};
|
};
|
||||||
const axiosSpy = createAxiosMock();
|
const expectedResp = Mock.of<ShortUrl>()
|
||||||
|
const axiosSpy = createAxiosMock({ data: expectedResp });
|
||||||
const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy, '', '');
|
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({
|
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
url: `/short-urls/${shortCode}`,
|
url: `/short-urls/${shortCode}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { Modal } from 'reactstrap';
|
||||||
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags';
|
import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
import { Modal } from 'reactstrap';
|
|
||||||
|
|
||||||
describe('<EditTagsModal />', () => {
|
describe('<EditTagsModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import reducer, {
|
||||||
EditShortUrlTagsAction,
|
EditShortUrlTagsAction,
|
||||||
} from '../../../src/short-urls/reducers/shortUrlTags';
|
} from '../../../src/short-urls/reducers/shortUrlTags';
|
||||||
import { ShlinkState } from '../../../src/container/types';
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
|
import { ReachableServer, SelectedServer } from '../../../src/servers/data';
|
||||||
|
|
||||||
describe('shortUrlTagsReducer', () => {
|
describe('shortUrlTagsReducer', () => {
|
||||||
const tags = [ 'foo', 'bar', 'baz' ];
|
const tags = [ 'foo', 'bar', 'baz' ];
|
||||||
|
@ -60,9 +61,10 @@ describe('shortUrlTagsReducer', () => {
|
||||||
|
|
||||||
describe('editShortUrlTags', () => {
|
describe('editShortUrlTags', () => {
|
||||||
const updateShortUrlTags = jest.fn();
|
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 dispatch = jest.fn();
|
||||||
const getState = () => Mock.all<ShlinkState>();
|
const buildGetState = (selectedServer?: SelectedServer) => () => Mock.of<ShlinkState>({ selectedServer });
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
@ -71,11 +73,12 @@ describe('shortUrlTagsReducer', () => {
|
||||||
|
|
||||||
updateShortUrlTags.mockResolvedValue(normalizedTags);
|
updateShortUrlTags.mockResolvedValue(normalizedTags);
|
||||||
|
|
||||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, domain, tags)(dispatch, getState);
|
await editShortUrlTags(buildShlinkApiClient)(shortCode, domain, tags)(dispatch, buildGetState());
|
||||||
|
|
||||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||||
expect(updateShortUrlTags).toHaveBeenCalledTimes(1);
|
expect(updateShortUrlTags).toHaveBeenCalledTimes(1);
|
||||||
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, domain, tags);
|
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, domain, tags);
|
||||||
|
expect(updateShortUrlMeta).not.toHaveBeenCalled();
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START });
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(
|
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<ReachableServer>({ 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 () => {
|
it('dispatches error on failure', async () => {
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
|
|
||||||
updateShortUrlTags.mockRejectedValue(error);
|
updateShortUrlTags.mockRejectedValue(error);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)(dispatch, getState);
|
await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)(dispatch, buildGetState());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toBe(error);
|
expect(e).toBe(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Marker, Popup } from 'react-leaflet';
|
import { Marker, Popup } from 'react-leaflet';
|
||||||
|
import { Modal } from 'reactstrap';
|
||||||
import MapModal from '../../../src/visits/helpers/MapModal';
|
import MapModal from '../../../src/visits/helpers/MapModal';
|
||||||
import { CityStats } from '../../../src/visits/types';
|
import { CityStats } from '../../../src/visits/types';
|
||||||
import { Modal } from 'reactstrap';
|
|
||||||
|
|
||||||
describe('<MapModal />', () => {
|
describe('<MapModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
Loading…
Reference in a new issue