diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df47f8d..f2dd09fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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). -## [Unreleased] +## [3.7.2] - 2022-08-07 ### Added * [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme. @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed * [#695](https://github.com/shlinkio/shlink-web-client/issues/695) Fixed some warnings in tests. * [#693](https://github.com/shlinkio/shlink-web-client/issues/693) Fixed tags, servers and domains search to make it case-insensitive. +* [#694](https://github.com/shlinkio/shlink-web-client/issues/694) Fixed editing and loading visits on short URLs with multi-segment slugs. ## [3.7.1] - 2022-05-25 diff --git a/src/short-urls/EditShortUrl.tsx b/src/short-urls/EditShortUrl.tsx index 72ad44cc..3c243a25 100644 --- a/src/short-urls/EditShortUrl.tsx +++ b/src/short-urls/EditShortUrl.tsx @@ -16,7 +16,7 @@ import { ShortUrlFormProps } from './ShortUrlForm'; import { ShortUrlDetail } from './reducers/shortUrlDetail'; import { EditShortUrlData } from './data'; import { ShortUrlEdition } from './reducers/shortUrlEdition'; -import { shortUrlDataFromShortUrl } from './helpers'; +import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers'; interface EditShortUrlConnectProps { settings: Settings; @@ -48,7 +48,7 @@ export const EditShortUrl = (ShortUrlForm: FC) => ({ const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle(); useEffect(() => { - params.shortCode && getShortUrlDetail(params.shortCode, domain); + params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain); }, []); if (loading) { diff --git a/src/short-urls/helpers/ShortUrlDetailLink.tsx b/src/short-urls/helpers/ShortUrlDetailLink.tsx index d656043f..82adc6ba 100644 --- a/src/short-urls/helpers/ShortUrlDetailLink.tsx +++ b/src/short-urls/helpers/ShortUrlDetailLink.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { Link } from 'react-router-dom'; import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data'; import { ShortUrl } from '../data'; +import { urlEncodeShortCode } from './index'; export type LinkSuffix = 'visits' | 'edit'; @@ -13,7 +14,7 @@ export interface ShortUrlDetailLinkProps { const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => { const query = domain ? `?domain=${domain}` : ''; - return `/server/${id}/short-code/${shortCode}/${suffix}${query}`; + return `/server/${id}/short-code/${urlEncodeShortCode(shortCode)}/${suffix}${query}`; }; export const ShortUrlDetailLink: FC> = ( diff --git a/src/short-urls/helpers/index.ts b/src/short-urls/helpers/index.ts index c8074507..e4dd1f09 100644 --- a/src/short-urls/helpers/index.ts +++ b/src/short-urls/helpers/index.ts @@ -40,3 +40,9 @@ export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUr validateUrl, }; }; + +const MULTI_SEGMENT_SEPARATOR = '__'; + +export const urlEncodeShortCode = (shortCode: string): string => shortCode.replaceAll('/', MULTI_SEGMENT_SEPARATOR); + +export const urlDecodeShortCode = (shortCode: string): string => shortCode.replaceAll(MULTI_SEGMENT_SEPARATOR, '/'); diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index 4fc4ec23..99bed7d3 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -13,6 +13,7 @@ import { VisitsStats } from './VisitsStats'; import { NormalizedVisit, VisitsParams } from './types'; import { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; +import { urlDecodeShortCode } from '../short-urls/helpers'; export interface ShortUrlVisitsProps extends CommonVisitsProps { getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; @@ -36,14 +37,14 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu const goBack = useGoBack(); const { domain } = parseQuery<{ domain?: string }>(search); const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => - getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback); + getShortUrlVisits(urlDecodeShortCode(shortCode), { ...toApiParams(params), domain }, doIntervalFallback); const exportCsv = (visits: NormalizedVisit[]) => exportVisits( `short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`, visits, ); useEffect(() => { - getShortUrlDetail(shortCode, domain); + getShortUrlDetail(urlDecodeShortCode(shortCode), domain); }, []); return ( @@ -59,4 +60,4 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu ); -}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]); +}, (_, params) => (params.shortCode ? [Topics.shortUrlVisits(urlDecodeShortCode(params.shortCode))] : [])); diff --git a/test/short-urls/helpers/index.test.ts b/test/short-urls/helpers/index.test.ts index 29fcf80d..06415123 100644 --- a/test/short-urls/helpers/index.test.ts +++ b/test/short-urls/helpers/index.test.ts @@ -1,6 +1,6 @@ import { Mock } from 'ts-mockery'; import { ShortUrl } from '../../../src/short-urls/data'; -import { shortUrlDataFromShortUrl } from '../../../src/short-urls/helpers'; +import { shortUrlDataFromShortUrl, urlDecodeShortCode, urlEncodeShortCode } from '../../../src/short-urls/helpers'; describe('helpers', () => { describe('shortUrlDataFromShortUrl', () => { @@ -25,4 +25,24 @@ describe('helpers', () => { expect(shortUrlDataFromShortUrl(shortUrl, settings)).toEqual(expectedInitialState); }); }); + + describe('urlEncodeShortCode', () => { + it.each([ + ['foo', 'foo'], + ['foo/bar', 'foo__bar'], + ['foo/bar/baz', 'foo__bar__baz'], + ])('parses shortCode as expected', (shortCode, result) => { + expect(urlEncodeShortCode(shortCode)).toEqual(result); + }); + }); + + describe('urlDecodeShortCode', () => { + it.each([ + ['foo', 'foo'], + ['foo__bar', 'foo/bar'], + ['foo__bar__baz', 'foo/bar/baz'], + ])('parses shortCode as expected', (shortCode, result) => { + expect(urlDecodeShortCode(shortCode)).toEqual(result); + }); + }); });