mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +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;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -202,7 +202,11 @@ const CreateShortUrl = (
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
|
||||
<CreateShortUrlResult
|
||||
{...shortUrlCreationResult}
|
||||
resetCreateShortUrl={resetCreateShortUrl}
|
||||
canBeClosed={basicMode}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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 { isNil } from 'ramda';
|
||||
import { useEffect } from 'react';
|
||||
|
@ -10,10 +11,11 @@ import './CreateShortUrlResult.scss';
|
|||
|
||||
export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
||||
resetCreateShortUrl: () => void;
|
||||
canBeClosed: boolean;
|
||||
}
|
||||
|
||||
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||
{ error, result, resetCreateShortUrl }: CreateShortUrlResultProps,
|
||||
{ error, result, resetCreateShortUrl, canBeClosed }: CreateShortUrlResultProps,
|
||||
) => {
|
||||
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
||||
|
||||
|
@ -38,6 +40,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
|||
return (
|
||||
<Card inverse className="bg-main mt-3">
|
||||
<CardBody>
|
||||
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
||||
|
||||
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface ShortUrlDeletion {
|
|||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
interface DeleteShortUrlAction extends Action<string> {
|
||||
export interface DeleteShortUrlAction extends Action<string> {
|
||||
shortCode: string;
|
||||
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 { shortUrlMatches } from '../helpers';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
||||
|
@ -8,10 +8,11 @@ import { GetState } from '../../container/types';
|
|||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||
import { ShlinkShortUrlsResponse } from '../../utils/services/types';
|
||||
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_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
||||
import { ShortUrlsListParams } from './shortUrlsListParams';
|
||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
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 = (
|
||||
ListShortUrlsAction & EditShortUrlTagsAction & ShortUrlEditedAction & ShortUrlMetaEditedAction & CreateVisitsAction
|
||||
ListShortUrlsAction
|
||||
& EditShortUrlTagsAction
|
||||
& ShortUrlEditedAction
|
||||
& ShortUrlMetaEditedAction
|
||||
& CreateVisitsAction
|
||||
& CreateShortUrlAction
|
||||
& DeleteShortUrlAction
|
||||
);
|
||||
|
||||
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_ERROR]: () => ({ loading: false, error: true }),
|
||||
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||
[SHORT_URL_DELETED]: (state, { shortCode, domain }) => !state.shortUrls ? state : assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
||||
state,
|
||||
[SHORT_URL_DELETED]: pipe(
|
||||
(state: ShortUrlsList, { shortCode, domain }: DeleteShortUrlAction) => !state.shortUrls ? state : assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
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_META_EDITED]: setPropFromActionOnMatchingShortUrl<ShortUrlMetaEditedAction>('meta'),
|
||||
|
@ -77,6 +91,20 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
|||
),
|
||||
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);
|
||||
|
||||
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 { ShortUrl } from '../../../src/short-urls/data';
|
||||
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('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 state = {
|
||||
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||
|
@ -103,6 +105,9 @@ describe('shortUrlsListReducer', () => {
|
|||
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||
],
|
||||
pagination: Mock.of<ShlinkPaginator>({
|
||||
totalItems: 10,
|
||||
}),
|
||||
}),
|
||||
loading: false,
|
||||
error: false,
|
||||
|
@ -111,6 +116,34 @@ describe('shortUrlsListReducer', () => {
|
|||
expect(reducer(state, { type: SHORT_URL_DELETED, shortCode } as any)).toEqual({
|
||||
shortUrls: {
|
||||
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,
|
||||
error: false,
|
||||
|
@ -147,6 +180,34 @@ describe('shortUrlsListReducer', () => {
|
|||
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', () => {
|
||||
|
|
Loading…
Reference in a new issue