Merge pull request #698 from acelaya-forks/feature/multi-segment-slugs

Feature/multi segment slugs
This commit is contained in:
Alejandro Celaya 2022-08-07 18:25:53 +02:00 committed by GitHub
commit 8fd419dc72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 37 additions and 8 deletions

View file

@ -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). 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 ### Added
* [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme. * [#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 ### Fixed
* [#695](https://github.com/shlinkio/shlink-web-client/issues/695) Fixed some warnings in tests. * [#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. * [#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 ## [3.7.1] - 2022-05-25

View file

@ -16,7 +16,7 @@ import { ShortUrlFormProps } from './ShortUrlForm';
import { ShortUrlDetail } from './reducers/shortUrlDetail'; import { ShortUrlDetail } from './reducers/shortUrlDetail';
import { EditShortUrlData } from './data'; import { EditShortUrlData } from './data';
import { ShortUrlEdition } from './reducers/shortUrlEdition'; import { ShortUrlEdition } from './reducers/shortUrlEdition';
import { shortUrlDataFromShortUrl } from './helpers'; import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';
interface EditShortUrlConnectProps { interface EditShortUrlConnectProps {
settings: Settings; settings: Settings;
@ -48,7 +48,7 @@ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle(); const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle();
useEffect(() => { useEffect(() => {
params.shortCode && getShortUrlDetail(params.shortCode, domain); params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain);
}, []); }, []);
if (loading) { if (loading) {

View file

@ -2,6 +2,7 @@ import { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data'; import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
import { ShortUrl } from '../data'; import { ShortUrl } from '../data';
import { urlEncodeShortCode } from './index';
export type LinkSuffix = 'visits' | 'edit'; export type LinkSuffix = 'visits' | 'edit';
@ -13,7 +14,7 @@ export interface ShortUrlDetailLinkProps {
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => { const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
const query = domain ? `?domain=${domain}` : ''; 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<ShortUrlDetailLinkProps & Record<string | number, any>> = ( export const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (

View file

@ -40,3 +40,9 @@ export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUr
validateUrl, 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, '/');

View file

@ -13,6 +13,7 @@ import { VisitsStats } from './VisitsStats';
import { NormalizedVisit, VisitsParams } from './types'; import { NormalizedVisit, VisitsParams } from './types';
import { CommonVisitsProps } from './types/CommonVisitsProps'; import { CommonVisitsProps } from './types/CommonVisitsProps';
import { toApiParams } from './types/helpers'; import { toApiParams } from './types/helpers';
import { urlDecodeShortCode } from '../short-urls/helpers';
export interface ShortUrlVisitsProps extends CommonVisitsProps { export interface ShortUrlVisitsProps extends CommonVisitsProps {
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
@ -36,14 +37,14 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
const goBack = useGoBack(); const goBack = useGoBack();
const { domain } = parseQuery<{ domain?: string }>(search); const { domain } = parseQuery<{ domain?: string }>(search);
const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) =>
getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback); getShortUrlVisits(urlDecodeShortCode(shortCode), { ...toApiParams(params), domain }, doIntervalFallback);
const exportCsv = (visits: NormalizedVisit[]) => exportVisits( const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`, `short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
visits, visits,
); );
useEffect(() => { useEffect(() => {
getShortUrlDetail(shortCode, domain); getShortUrlDetail(urlDecodeShortCode(shortCode), domain);
}, []); }, []);
return ( return (
@ -59,4 +60,4 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} /> <ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
</VisitsStats> </VisitsStats>
); );
}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]); }, (_, params) => (params.shortCode ? [Topics.shortUrlVisits(urlDecodeShortCode(params.shortCode))] : []));

View file

@ -1,6 +1,6 @@
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { ShortUrl } from '../../../src/short-urls/data'; 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('helpers', () => {
describe('shortUrlDataFromShortUrl', () => { describe('shortUrlDataFromShortUrl', () => {
@ -25,4 +25,24 @@ describe('helpers', () => {
expect(shortUrlDataFromShortUrl(shortUrl, settings)).toEqual(expectedInitialState); 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);
});
});
}); });