mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Implemented reducers for actions affecting short URLs list
This commit is contained in:
parent
17d5c4327b
commit
8d5f7e942d
6 changed files with 112 additions and 12 deletions
|
@ -52,6 +52,10 @@ body,
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -202,7 +202,11 @@ const CreateShortUrl = (
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
|
<CreateShortUrlResult
|
||||||
|
{...shortUrlCreationResult}
|
||||||
|
resetCreateShortUrl={resetCreateShortUrl}
|
||||||
|
canBeClosed={basicMode}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { faTimes as closeIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isNil } from 'ramda';
|
import { isNil } from 'ramda';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
@ -10,10 +11,11 @@ import './CreateShortUrlResult.scss';
|
||||||
|
|
||||||
export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
||||||
resetCreateShortUrl: () => void;
|
resetCreateShortUrl: () => void;
|
||||||
|
canBeClosed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||||
{ error, result, resetCreateShortUrl }: CreateShortUrlResultProps,
|
{ error, result, resetCreateShortUrl, canBeClosed }: CreateShortUrlResultProps,
|
||||||
) => {
|
) => {
|
||||||
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
||||||
|
|
||||||
|
@ -38,6 +40,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||||
return (
|
return (
|
||||||
<Card inverse className="bg-main mt-3">
|
<Card inverse className="bg-main mt-3">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
||||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
||||||
|
|
||||||
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
||||||
|
|
|
@ -18,7 +18,7 @@ export interface ShortUrlDeletion {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteShortUrlAction extends Action<string> {
|
export interface DeleteShortUrlAction extends Action<string> {
|
||||||
shortCode: string;
|
shortCode: string;
|
||||||
domain?: string | null;
|
domain?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { assoc, assocPath, last, reject } from 'ramda';
|
import { assoc, assocPath, init, last, pipe, reject } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { shortUrlMatches } from '../helpers';
|
import { shortUrlMatches } from '../helpers';
|
||||||
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
||||||
|
@ -8,10 +8,11 @@ import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
import { ShlinkShortUrlsResponse } from '../../utils/services/types';
|
import { ShlinkShortUrlsResponse } from '../../utils/services/types';
|
||||||
import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||||
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||||
import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction } from './shortUrlMeta';
|
import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction } from './shortUrlMeta';
|
||||||
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
||||||
import { ShortUrlsListParams } from './shortUrlsListParams';
|
import { ShortUrlsListParams } from './shortUrlsListParams';
|
||||||
|
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
||||||
|
@ -31,7 +32,13 @@ export interface ListShortUrlsAction extends Action<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListShortUrlsCombinedAction = (
|
export type ListShortUrlsCombinedAction = (
|
||||||
ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitsAction
|
ListShortUrlsAction
|
||||||
|
& EditShortUrlTagsAction
|
||||||
|
& ShortUrlEditedAction
|
||||||
|
& ShortUrlMetaEditedAction
|
||||||
|
& CreateVisitsAction
|
||||||
|
& CreateShortUrlAction
|
||||||
|
& DeleteShortUrlAction
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialState: ShortUrlsList = {
|
const initialState: ShortUrlsList = {
|
||||||
|
@ -55,10 +62,17 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
||||||
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }),
|
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||||
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
|
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
|
||||||
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||||
[SHORT_URL_DELETED]: (state, { shortCode, domain }) => !state.shortUrls ? state : assocPath(
|
[SHORT_URL_DELETED]: pipe(
|
||||||
[ 'shortUrls', 'data' ],
|
(state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => !state.shortUrls ? state : assocPath(
|
||||||
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
[ 'shortUrls', 'data' ],
|
||||||
state,
|
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
(state) => !state.shortUrls ? state : assocPath(
|
||||||
|
[ 'shortUrls', 'pagination', 'totalItems' ],
|
||||||
|
state.shortUrls.pagination.totalItems - 1,
|
||||||
|
state,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
[SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl<EditShortUrlTagsAction>('tags'),
|
[SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl<EditShortUrlTagsAction>('tags'),
|
||||||
[SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl<ShortUrlMetaEditedAction>('meta'),
|
[SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl<ShortUrlMetaEditedAction>('meta'),
|
||||||
|
@ -77,6 +91,20 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
||||||
),
|
),
|
||||||
state,
|
state,
|
||||||
),
|
),
|
||||||
|
[CREATE_SHORT_URL]: pipe(
|
||||||
|
// The only place where the list and the creation form coexist is the overview page.
|
||||||
|
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL and remove the last one.
|
||||||
|
(state: ShortUrlsList, { result }: CreateShortUrlAction) => !state.shortUrls ? state : assocPath(
|
||||||
|
[ 'shortUrls', 'data' ],
|
||||||
|
[ result, ...init(state.shortUrls.data) ],
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
(state: ShortUrlsList) => !state.shortUrls ? state : assocPath(
|
||||||
|
[ 'shortUrls', 'pagination', 'totalItems' ],
|
||||||
|
state.shortUrls.pagination.totalItems + 1,
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
|
|
|
@ -11,7 +11,9 @@ import { SHORT_URL_META_EDITED } from '../../../src/short-urls/reducers/shortUrl
|
||||||
import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation';
|
import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||||
import { ShlinkShortUrlsResponse } from '../../../src/utils/services/types';
|
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/utils/services/types';
|
||||||
|
import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||||
|
import { SHORT_URL_EDITED } from '../../../src/short-urls/reducers/shortUrlEdition';
|
||||||
|
|
||||||
describe('shortUrlsListReducer', () => {
|
describe('shortUrlsListReducer', () => {
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
|
@ -94,7 +96,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes matching URL on SHORT_URL_DELETED', () => {
|
it('removes matching URL and reduces total on SHORT_URL_DELETED', () => {
|
||||||
const shortCode = 'abc123';
|
const shortCode = 'abc123';
|
||||||
const state = {
|
const state = {
|
||||||
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||||
|
@ -103,6 +105,9 @@ describe('shortUrlsListReducer', () => {
|
||||||
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
|
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
|
||||||
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||||
],
|
],
|
||||||
|
pagination: Mock.of<ShlinkPaginator>({
|
||||||
|
totalItems: 10,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -111,6 +116,34 @@ describe('shortUrlsListReducer', () => {
|
||||||
expect(reducer(state, { type: SHORT_URL_DELETED, shortCode } as any)).toEqual({
|
expect(reducer(state, { type: SHORT_URL_DELETED, shortCode } as any)).toEqual({
|
||||||
shortUrls: {
|
shortUrls: {
|
||||||
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
||||||
|
pagination: { totalItems: 9 },
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates edited short URL on SHORT_URL_EDITED', () => {
|
||||||
|
const shortCode = 'abc123';
|
||||||
|
const state = {
|
||||||
|
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||||
|
data: [
|
||||||
|
Mock.of<ShortUrl>({ shortCode, longUrl: 'old' }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode, domain: 'example.com', longUrl: 'foo' }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode: 'foo', longUrl: 'bar' }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(reducer(state, { type: SHORT_URL_EDITED, shortCode, longUrl: 'newValue' } as any)).toEqual({
|
||||||
|
shortUrls: {
|
||||||
|
data: [
|
||||||
|
{ shortCode, longUrl: 'newValue' },
|
||||||
|
{ shortCode, longUrl: 'foo', domain: 'example.com' },
|
||||||
|
{ shortCode: 'foo', longUrl: 'bar' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
|
@ -147,6 +180,34 @@ describe('shortUrlsListReducer', () => {
|
||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prepends new short URL and increases total on CREATE_SHORT_URL', () => {
|
||||||
|
const newShortUrl = Mock.of<ShortUrl>({ shortCode: 'newOne' });
|
||||||
|
const shortCode = 'abc123';
|
||||||
|
const state = {
|
||||||
|
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||||
|
data: [
|
||||||
|
Mock.of<ShortUrl>({ shortCode }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
|
||||||
|
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||||
|
],
|
||||||
|
pagination: Mock.of<ShlinkPaginator>({
|
||||||
|
totalItems: 15,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(reducer(state, { type: CREATE_SHORT_URL, result: newShortUrl } as any)).toEqual({
|
||||||
|
shortUrls: {
|
||||||
|
data: [{ shortCode: 'newOne' }, { shortCode }, { shortCode, domain: 'example.com' }],
|
||||||
|
pagination: { totalItems: 16 },
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('listShortUrls', () => {
|
describe('listShortUrls', () => {
|
||||||
|
|
Loading…
Reference in a new issue