mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-12 18:43:50 +00:00
Created view to edit short URLs
This commit is contained in:
@@ -21,6 +21,7 @@ const MenuLayout = (
|
|||||||
OrphanVisits: FC,
|
OrphanVisits: FC,
|
||||||
ServerError: FC,
|
ServerError: FC,
|
||||||
Overview: FC,
|
Overview: FC,
|
||||||
|
EditShortUrl: FC,
|
||||||
) => withSelectedServer(({ location, selectedServer }) => {
|
) => withSelectedServer(({ location, selectedServer }) => {
|
||||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ const MenuLayout = (
|
|||||||
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
||||||
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
||||||
<Route path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
<Route path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||||
|
<Route path="/server/:serverId/short-code/:shortCode/edit" component={EditShortUrl} />
|
||||||
{addTagsVisitsRoute && <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
{addTagsVisitsRoute && <Route path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
||||||
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
|
{addOrphanVisitsRoute && <Route path="/server/:serverId/orphan-visits" component={OrphanVisits} />}
|
||||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
|||||||
'OrphanVisits',
|
'OrphanVisits',
|
||||||
'ServerError',
|
'ServerError',
|
||||||
'Overview',
|
'Overview',
|
||||||
|
'EditShortUrl',
|
||||||
);
|
);
|
||||||
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
||||||
bottle.decorator('MenuLayout', withRouter);
|
bottle.decorator('MenuLayout', withRouter);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { pipe, replace, trim } from 'ramda';
|
|
||||||
import { FC, useMemo } from 'react';
|
import { FC, useMemo } from 'react';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
||||||
@@ -19,8 +18,6 @@ interface CreateShortUrlConnectProps extends CreateShortUrlProps {
|
|||||||
resetCreateShortUrl: () => void;
|
resetCreateShortUrl: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
|
||||||
|
|
||||||
const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ({
|
const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ({
|
||||||
longUrl: '',
|
longUrl: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
|
|||||||
76
src/short-urls/EditShortUrl.tsx
Normal file
76
src/short-urls/EditShortUrl.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { FC, useEffect } from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { SelectedServer } from '../servers/data';
|
||||||
|
import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings';
|
||||||
|
import { OptionalString } from '../utils/utils';
|
||||||
|
import { parseQuery } from '../utils/helpers/query';
|
||||||
|
import Message from '../utils/Message';
|
||||||
|
import { Result } from '../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
|
import { ShortUrlFormProps } from './ShortUrlForm';
|
||||||
|
import { ShortUrlDetail } from './reducers/shortUrlDetail';
|
||||||
|
import { ShortUrl, ShortUrlData } from './data';
|
||||||
|
|
||||||
|
interface EditShortUrlConnectProps extends RouteComponentProps<{ shortCode: string }> {
|
||||||
|
settings: Settings;
|
||||||
|
selectedServer: SelectedServer;
|
||||||
|
shortUrlDetail: ShortUrlDetail;
|
||||||
|
getShortUrlDetail: (shortCode: string, domain: OptionalString) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
|
||||||
|
const validateUrl = settings?.validateUrls ?? false;
|
||||||
|
|
||||||
|
if (!shortUrl) {
|
||||||
|
return { longUrl: '', validateUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
longUrl: shortUrl.longUrl,
|
||||||
|
tags: shortUrl.tags,
|
||||||
|
title: shortUrl.title ?? undefined,
|
||||||
|
domain: shortUrl.domain ?? undefined,
|
||||||
|
validSince: shortUrl.meta.validSince ?? undefined,
|
||||||
|
validUntil: shortUrl.meta.validUntil ?? undefined,
|
||||||
|
maxVisits: shortUrl.meta.maxVisits ?? undefined,
|
||||||
|
validateUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({
|
||||||
|
match: { params },
|
||||||
|
location: { search },
|
||||||
|
settings: { shortUrlCreation: shortUrlCreationSettings },
|
||||||
|
selectedServer,
|
||||||
|
shortUrlDetail,
|
||||||
|
getShortUrlDetail,
|
||||||
|
}: EditShortUrlConnectProps) => {
|
||||||
|
const { loading, error, errorData, shortUrl } = shortUrlDetail;
|
||||||
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getShortUrlDetail(params.shortCode, domain);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Message loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Result type="error">
|
||||||
|
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while loading short URL detail :(" />
|
||||||
|
</Result>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShortUrlForm
|
||||||
|
initialState={getInitialState(shortUrl, shortUrlCreationSettings)}
|
||||||
|
saving={false}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
mode="edit"
|
||||||
|
onSave={async (shortUrlData) => Promise.resolve(console.log(shortUrlData))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
import { InputType } from 'reactstrap/lib/Input';
|
||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import { isEmpty } from 'ramda';
|
import { isEmpty, pipe, replace, trim } from 'ramda';
|
||||||
import * as m from 'moment';
|
import * as m from 'moment';
|
||||||
import DateInput, { DateInputProps } from '../utils/DateInput';
|
import DateInput, { DateInputProps } from '../utils/DateInput';
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +18,6 @@ import { Versions } from '../utils/helpers/version';
|
|||||||
import { DomainSelectorProps } from '../domains/DomainSelector';
|
import { DomainSelectorProps } from '../domains/DomainSelector';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||||
import { normalizeTag } from './CreateShortUrl';
|
|
||||||
import { ShortUrlData } from './data';
|
import { ShortUrlData } from './data';
|
||||||
import './ShortUrlForm.scss';
|
import './ShortUrlForm.scss';
|
||||||
|
|
||||||
@@ -34,19 +33,26 @@ export interface ShortUrlFormProps {
|
|||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
|
|
||||||
export const ShortUrlForm = (
|
export const ShortUrlForm = (
|
||||||
TagsSelector: FC<TagsSelectorProps>,
|
TagsSelector: FC<TagsSelectorProps>,
|
||||||
ForServerVersion: FC<Versions>,
|
ForServerVersion: FC<Versions>,
|
||||||
DomainSelector: FC<DomainSelectorProps>,
|
DomainSelector: FC<DomainSelectorProps>,
|
||||||
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer, children }) => { // eslint-disable-line complexity
|
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState, selectedServer, children }) => { // eslint-disable-line complexity
|
||||||
const [ shortUrlData, setShortUrlData ] = useState(initialState);
|
const [ shortUrlData, setShortUrlData ] = useState(initialState);
|
||||||
|
const isEdit = mode === 'edit';
|
||||||
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
|
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
|
||||||
const reset = () => setShortUrlData(initialState);
|
const reset = () => setShortUrlData(initialState);
|
||||||
const submit = handleEventPreventingDefault(async () => onSave({
|
const submit = handleEventPreventingDefault(async () => onSave({
|
||||||
...shortUrlData,
|
...shortUrlData,
|
||||||
validSince: formatIsoDate(shortUrlData.validSince) ?? undefined,
|
validSince: formatIsoDate(shortUrlData.validSince) ?? undefined,
|
||||||
validUntil: formatIsoDate(shortUrlData.validUntil) ?? undefined,
|
validUntil: formatIsoDate(shortUrlData.validUntil) ?? undefined,
|
||||||
}).then(reset).catch(() => {}));
|
}).then(() => !isEdit && reset()).catch(() => {}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShortUrlData(initialState);
|
||||||
|
}, [ initialState ]);
|
||||||
|
|
||||||
const renderOptionalInput = (id: NonDateFields, placeholder: string, type: InputType = 'text', props = {}) => (
|
const renderOptionalInput = (id: NonDateFields, placeholder: string, type: InputType = 'text', props = {}) => (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
@@ -54,7 +60,7 @@ export const ShortUrlForm = (
|
|||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={shortUrlData[id]}
|
value={shortUrlData[id] ?? ''}
|
||||||
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
|
onChange={(e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value })}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -93,7 +99,6 @@ export const ShortUrlForm = (
|
|||||||
const showDomainSelector = supportsListingDomains(selectedServer);
|
const showDomainSelector = supportsListingDomains(selectedServer);
|
||||||
const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer);
|
const disableShortCodeLength = !supportsSettingShortCodeLength(selectedServer);
|
||||||
const supportsTitle = supportsShortUrlTitle(selectedServer);
|
const supportsTitle = supportsShortUrlTitle(selectedServer);
|
||||||
const isEdit = mode === 'edit';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="short-url-form" onSubmit={submit}>
|
<form className="short-url-form" onSubmit={submit}>
|
||||||
@@ -191,7 +196,7 @@ export const ShortUrlForm = (
|
|||||||
disabled={saving || isEmpty(shortUrlData.longUrl)}
|
disabled={saving || isEmpty(shortUrlData.longUrl)}
|
||||||
className="btn-xs-block"
|
className="btn-xs-block"
|
||||||
>
|
>
|
||||||
{saving ? 'Creating...' : 'Create'}
|
{saving ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
30
src/short-urls/helpers/ShortUrlDetailLink.tsx
Normal file
30
src/short-urls/helpers/ShortUrlDetailLink.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
|
||||||
|
import { ShortUrl } from '../data';
|
||||||
|
|
||||||
|
export type LinkSuffix = 'visits' | 'edit';
|
||||||
|
|
||||||
|
export interface ShortUrlDetailLinkProps {
|
||||||
|
shortUrl?: ShortUrl | null;
|
||||||
|
selectedServer?: SelectedServer;
|
||||||
|
suffix: LinkSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
|
||||||
|
const query = domain ? `?domain=${domain}` : '';
|
||||||
|
|
||||||
|
return `/server/${id}/short-code/${shortCode}/${suffix}${query}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
|
||||||
|
{ selectedServer, shortUrl, suffix, children, ...rest },
|
||||||
|
) => {
|
||||||
|
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
|
||||||
|
return <span {...rest}>{children}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Link to={buildUrl(selectedServer, shortUrl, suffix)} {...rest}>{children}</Link>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShortUrlDetailLink;
|
||||||
@@ -4,10 +4,14 @@ import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
|||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import VisitStatsLink, { VisitStatsLinkProps } from './VisitStatsLink';
|
import { ShortUrl } from '../data';
|
||||||
|
import { SelectedServer } from '../../servers/data';
|
||||||
|
import ShortUrlDetailLink from './ShortUrlDetailLink';
|
||||||
import './ShortUrlVisitsCount.scss';
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
|
||||||
interface ShortUrlVisitsCountProps extends VisitStatsLinkProps {
|
interface ShortUrlVisitsCountProps {
|
||||||
|
shortUrl?: ShortUrl | null;
|
||||||
|
selectedServer?: SelectedServer;
|
||||||
visitsCount: number;
|
visitsCount: number;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
@@ -15,13 +19,13 @@ interface ShortUrlVisitsCountProps extends VisitStatsLinkProps {
|
|||||||
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps) => {
|
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps) => {
|
||||||
const maxVisits = shortUrl?.meta?.maxVisits;
|
const maxVisits = shortUrl?.meta?.maxVisits;
|
||||||
const visitsLink = (
|
const visitsLink = (
|
||||||
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||||
<strong
|
<strong
|
||||||
className={classNames('short-url-visits-count__amount', { 'short-url-visits-count__amount--big': active })}
|
className={classNames('short-url-visits-count__amount', { 'short-url-visits-count__amount--big': active })}
|
||||||
>
|
>
|
||||||
{prettify(visitsCount)}
|
{prettify(visitsCount)}
|
||||||
</strong>
|
</strong>
|
||||||
</VisitStatsLink>
|
</ShortUrlDetailLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!maxVisits) {
|
if (!maxVisits) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useToggle } from '../../utils/helpers/hooks';
|
|||||||
import { ShortUrl, ShortUrlModalProps } from '../data';
|
import { ShortUrl, ShortUrlModalProps } from '../data';
|
||||||
import { Versions } from '../../utils/helpers/version';
|
import { Versions } from '../../utils/helpers/version';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import VisitStatsLink from './VisitStatsLink';
|
import ShortUrlDetailLink from './ShortUrlDetailLink';
|
||||||
import './ShortUrlsRowMenu.scss';
|
import './ShortUrlsRowMenu.scss';
|
||||||
|
|
||||||
export interface ShortUrlsRowMenuProps {
|
export interface ShortUrlsRowMenuProps {
|
||||||
@@ -44,10 +44,14 @@ const ShortUrlsRowMenu = (
|
|||||||
<FontAwesomeIcon icon={menuIcon} />
|
<FontAwesomeIcon icon={menuIcon} />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu right>
|
<DropdownMenu right>
|
||||||
<DropdownItem tag={VisitStatsLink} selectedServer={selectedServer} shortUrl={shortUrl}>
|
<DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
|
<DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="edit">
|
||||||
|
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit short URL
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
<DropdownItem onClick={toggleTags}>
|
<DropdownItem onClick={toggleTags}>
|
||||||
<FontAwesomeIcon icon={tagsIcon} fixedWidth /> Edit tags
|
<FontAwesomeIcon icon={tagsIcon} fixedWidth /> Edit tags
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data';
|
|
||||||
import { ShortUrl } from '../data';
|
|
||||||
|
|
||||||
export interface VisitStatsLinkProps {
|
|
||||||
shortUrl?: ShortUrl | null;
|
|
||||||
selectedServer?: SelectedServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildVisitsUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl) => {
|
|
||||||
const query = domain ? `?domain=${domain}` : '';
|
|
||||||
|
|
||||||
return `/server/${id}/short-code/${shortCode}/visits${query}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VisitStatsLink: FC<VisitStatsLinkProps & Record<string | number, any>> = (
|
|
||||||
{ selectedServer, shortUrl, children, ...rest },
|
|
||||||
) => {
|
|
||||||
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
|
|
||||||
return <span {...rest}>{children}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Link to={buildVisitsUrl(selectedServer, shortUrl)} {...rest}>{children}</Link>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VisitStatsLink;
|
|
||||||
@@ -5,6 +5,8 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
|||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { shortUrlMatches } from '../helpers';
|
import { shortUrlMatches } from '../helpers';
|
||||||
|
import { ProblemDetailsError } from '../../api/types';
|
||||||
|
import { parseApiError } from '../../api/utils';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
|
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
|
||||||
@@ -16,20 +18,25 @@ export interface ShortUrlDetail {
|
|||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShortUrlDetailAction extends Action<string> {
|
export interface ShortUrlDetailAction extends Action<string> {
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlDetailFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlDetail = {
|
const initialState: ShortUrlDetail = {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlDetail, ShortUrlDetailAction>({
|
export default buildReducer<ShortUrlDetail, ShortUrlDetailAction & ShortUrlDetailFailedAction>({
|
||||||
[GET_SHORT_URL_DETAIL_START]: () => ({ loading: true, error: false }),
|
[GET_SHORT_URL_DETAIL_START]: () => ({ loading: true, error: false }),
|
||||||
[GET_SHORT_URL_DETAIL_ERROR]: () => ({ loading: false, error: true }),
|
[GET_SHORT_URL_DETAIL_ERROR]: (_, { errorData }) => ({ loading: false, error: true, errorData }),
|
||||||
[GET_SHORT_URL_DETAIL]: (_, { shortUrl }) => ({ shortUrl, ...initialState }),
|
[GET_SHORT_URL_DETAIL]: (_, { shortUrl }) => ({ shortUrl, ...initialState }),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
@@ -47,6 +54,6 @@ export const getShortUrlDetail = (buildShlinkApiClient: ShlinkApiClientBuilder)
|
|||||||
|
|
||||||
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: GET_SHORT_URL_DETAIL_ERROR });
|
dispatch<ShortUrlDetailFailedAction>({ type: GET_SHORT_URL_DETAIL_ERROR, errorData: parseApiError(e) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { ConnectDecorator } from '../../container/types';
|
|||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
import QrCodeModal from '../helpers/QrCodeModal';
|
import QrCodeModal from '../helpers/QrCodeModal';
|
||||||
import { ShortUrlForm } from '../ShortUrlForm';
|
import { ShortUrlForm } from '../ShortUrlForm';
|
||||||
|
import { EditShortUrl } from '../EditShortUrl';
|
||||||
|
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
@@ -54,6 +56,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
connect([ 'shortUrlCreationResult', 'selectedServer', 'settings' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
connect([ 'shortUrlCreationResult', 'selectedServer', 'settings' ], [ 'createShortUrl', 'resetCreateShortUrl' ]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bottle.serviceFactory('EditShortUrl', EditShortUrl, 'ShortUrlForm');
|
||||||
|
bottle.decorator(
|
||||||
|
'EditShortUrl',
|
||||||
|
connect([ 'shortUrlDetail', 'selectedServer', 'settings' ], [ 'getShortUrlDetail' ]),
|
||||||
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
||||||
bottle.decorator('DeleteShortUrlModal', connect([ 'shortUrlDeletion' ], [ 'deleteShortUrl', 'resetDeleteShortUrl' ]));
|
bottle.decorator('DeleteShortUrlModal', connect([ 'shortUrlDeletion' ], [ 'deleteShortUrl', 'resetDeleteShortUrl' ]));
|
||||||
|
|
||||||
@@ -89,6 +97,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient');
|
bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta);
|
bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta);
|
||||||
|
|
||||||
|
bottle.serviceFactory('getShortUrlDetail', getShortUrlDetail, 'buildShlinkApiClient');
|
||||||
|
|
||||||
bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient');
|
bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import ShortUrlVisits from '../ShortUrlVisits';
|
import ShortUrlVisits from '../ShortUrlVisits';
|
||||||
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
||||||
import { getShortUrlDetail } from '../../short-urls/reducers/shortUrlDetail';
|
|
||||||
import MapModal from '../helpers/MapModal';
|
import MapModal from '../helpers/MapModal';
|
||||||
import { createNewVisits } from '../reducers/visitCreation';
|
import { createNewVisits } from '../reducers/visitCreation';
|
||||||
import TagVisits from '../TagVisits';
|
import TagVisits from '../TagVisits';
|
||||||
@@ -41,7 +40,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient');
|
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('getShortUrlDetail', getShortUrlDetail, 'buildShlinkApiClient');
|
|
||||||
bottle.serviceFactory('cancelGetShortUrlVisits', () => cancelGetShortUrlVisits);
|
bottle.serviceFactory('cancelGetShortUrlVisits', () => cancelGetShortUrlVisits);
|
||||||
|
|
||||||
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { SemVer } from '../../src/utils/helpers/version';
|
|||||||
describe('<MenuLayout />', () => {
|
describe('<MenuLayout />', () => {
|
||||||
const ServerError = jest.fn();
|
const ServerError = jest.fn();
|
||||||
const C = jest.fn();
|
const C = jest.fn();
|
||||||
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, ServerError, C);
|
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, ServerError, C, C);
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (selectedServer: SelectedServer) => {
|
const createWrapper = (selectedServer: SelectedServer) => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
@@ -49,11 +49,11 @@ describe('<MenuLayout />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ '2.1.0' as SemVer, 6 ],
|
[ '2.1.0' as SemVer, 7 ],
|
||||||
[ '2.2.0' as SemVer, 7 ],
|
[ '2.2.0' as SemVer, 8 ],
|
||||||
[ '2.5.0' as SemVer, 7 ],
|
[ '2.5.0' as SemVer, 8 ],
|
||||||
[ '2.6.0' as SemVer, 8 ],
|
[ '2.6.0' as SemVer, 9 ],
|
||||||
[ '2.7.0' as SemVer, 8 ],
|
[ '2.7.0' as SemVer, 9 ],
|
||||||
])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => {
|
])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => {
|
||||||
const selectedServer = Mock.of<ReachableServer>({ version });
|
const selectedServer = Mock.of<ReachableServer>({ version });
|
||||||
const wrapper = createWrapper(selectedServer).dive();
|
const wrapper = createWrapper(selectedServer).dive();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import VisitStatsLink from '../../../src/short-urls/helpers/VisitStatsLink';
|
import ShortUrlDetailLink, { LinkSuffix } from '../../../src/short-urls/helpers/ShortUrlDetailLink';
|
||||||
import { NotFoundServer, ReachableServer } from '../../../src/servers/data';
|
import { NotFoundServer, ReachableServer } from '../../../src/servers/data';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<VisitStatsLink />', () => {
|
describe('<ShortUrlDetailLink />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
@@ -19,7 +19,11 @@ describe('<VisitStatsLink />', () => {
|
|||||||
[ null, Mock.all<ShortUrl>() ],
|
[ null, Mock.all<ShortUrl>() ],
|
||||||
[ undefined, Mock.all<ShortUrl>() ],
|
[ undefined, Mock.all<ShortUrl>() ],
|
||||||
])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => {
|
])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => {
|
||||||
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
|
wrapper = shallow(
|
||||||
|
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||||
|
Something
|
||||||
|
</ShortUrlDetailLink>,
|
||||||
|
);
|
||||||
const link = wrapper.find(Link);
|
const link = wrapper.find(Link);
|
||||||
|
|
||||||
expect(link).toHaveLength(0);
|
expect(link).toHaveLength(0);
|
||||||
@@ -30,15 +34,33 @@ describe('<VisitStatsLink />', () => {
|
|||||||
[
|
[
|
||||||
Mock.of<ReachableServer>({ id: '1' }),
|
Mock.of<ReachableServer>({ id: '1' }),
|
||||||
Mock.of<ShortUrl>({ shortCode: 'abc123' }),
|
Mock.of<ShortUrl>({ shortCode: 'abc123' }),
|
||||||
|
'visits' as LinkSuffix,
|
||||||
'/server/1/short-code/abc123/visits',
|
'/server/1/short-code/abc123/visits',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Mock.of<ReachableServer>({ id: '3' }),
|
Mock.of<ReachableServer>({ id: '3' }),
|
||||||
Mock.of<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
|
Mock.of<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
|
||||||
|
'visits' as LinkSuffix,
|
||||||
'/server/3/short-code/def456/visits?domain=example.com',
|
'/server/3/short-code/def456/visits?domain=example.com',
|
||||||
],
|
],
|
||||||
])('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => {
|
[
|
||||||
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
|
Mock.of<ReachableServer>({ id: '1' }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode: 'abc123' }),
|
||||||
|
'edit' as LinkSuffix,
|
||||||
|
'/server/1/short-code/abc123/edit',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Mock.of<ReachableServer>({ id: '3' }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
|
||||||
|
'edit' as LinkSuffix,
|
||||||
|
'/server/3/short-code/def456/edit?domain=example.com',
|
||||||
|
],
|
||||||
|
])('renders link with expected query when', (selectedServer, shortUrl, suffix, expectedLink) => {
|
||||||
|
wrapper = shallow(
|
||||||
|
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix={suffix}>
|
||||||
|
Something
|
||||||
|
</ShortUrlDetailLink>,
|
||||||
|
);
|
||||||
const link = wrapper.find(Link);
|
const link = wrapper.find(Link);
|
||||||
const to = link.prop('to');
|
const to = link.prop('to');
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ describe('<ShortUrlsRowMenu />', () => {
|
|||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const items = wrapper.find(DropdownItem);
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
expect(items).toHaveLength(7);
|
expect(items).toHaveLength(8);
|
||||||
expect(items.find('[divider]')).toHaveLength(1);
|
expect(items.find('[divider]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('shortUrlDetailReducer', () => {
|
|||||||
const buildGetState = (shortUrlsList?: ShortUrlsList) => () => Mock.of<ShlinkState>({ shortUrlsList });
|
const buildGetState = (shortUrlsList?: ShortUrlsList) => () => Mock.of<ShlinkState>({ shortUrlsList });
|
||||||
|
|
||||||
it('dispatches start and error when promise is rejected', async () => {
|
it('dispatches start and error when promise is rejected', async () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
const ShlinkApiClient = buildApiClientMock(Promise.reject({}));
|
||||||
|
|
||||||
await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, buildGetState());
|
await getShortUrlDetail(() => ShlinkApiClient)('abc123', '')(dispatchMock, buildGetState());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user