mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-12 11:17:25 +03:00
commit
e9fcdcb049
9 changed files with 103 additions and 18 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -4,6 +4,25 @@ 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).
|
||||
|
||||
## [3.5.1] - 2022-01-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#555](https://github.com/shlinkio/shlink-web-client/issues/555) Fixed vertical alignment in welcome screen logo.
|
||||
* [#554](https://github.com/shlinkio/shlink-web-client/issues/554) Fixed behavior in overview page, where items in the list of short URLs were stripped out when creating new ones, even if the amount of short URLs was still not yet big enough.
|
||||
* [#557](https://github.com/shlinkio/shlink-web-client/issues/557) Fixed new tags added to new short URLs, not appearing on tags autosuggest.
|
||||
|
||||
|
||||
## [3.5.0] - 2022-01-01
|
||||
### Added
|
||||
* [#407](https://github.com/shlinkio/shlink-web-client/pull/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter".
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
||||
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink-web-client.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
||||
[![Twitter](https://img.shields.io/twitter/follow/shlinkio?color=blue&label=follow&logo=twitter&style=flat-square)](https://twitter.com/shlinkio)
|
||||
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://slnk.to/donate)
|
||||
|
||||
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
||||
|
|
|
@ -12,8 +12,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.home__logo-wrapper {
|
||||
padding: 1.5rem !important;
|
||||
height: 100% !important;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.home__logo {
|
||||
@include vertical-align();
|
||||
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.home__main-card {
|
||||
|
@ -25,6 +33,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.home__title-wrapper {
|
||||
padding: 1.5rem !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.home__title {
|
||||
text-align: center;
|
||||
font-size: 1.75rem;
|
||||
|
|
|
@ -30,12 +30,14 @@ const Home = ({ servers, history }: HomeProps) => {
|
|||
<Card className="home__main-card">
|
||||
<Row noGutters>
|
||||
<div className="col-md-5 d-none d-md-block">
|
||||
<div className="p-4">
|
||||
<ShlinkLogo />
|
||||
<div className="home__logo-wrapper">
|
||||
<div className="home__logo">
|
||||
<ShlinkLogo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-7 home__servers-container">
|
||||
<div className="p-4">
|
||||
<div className="home__title-wrapper">
|
||||
<h1 className="home__title">Welcome!</h1>
|
||||
</div>
|
||||
<ServersListGroup embedded servers={serversList}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { Card, CardBody, CardHeader, CardText, CardTitle, Row } from 'reactstrap';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
||||
import { ITEMS_IN_OVERVIEW_PAGE, ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
||||
import { prettify } from '../utils/helpers/numbers';
|
||||
import { TagsList } from '../tags/reducers/tagsList';
|
||||
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
|
||||
|
@ -44,7 +44,7 @@ export const Overview = (
|
|||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
listShortUrls({ itemsPerPage: 5, orderBy: { field: 'dateCreated', dir: 'DESC' } });
|
||||
listShortUrls({ itemsPerPage: ITEMS_IN_OVERVIEW_PAGE, orderBy: { field: 'dateCreated', dir: 'DESC' } });
|
||||
listTags();
|
||||
loadVisitsOverview();
|
||||
}, []);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { assoc, assocPath, init, last, pipe, reject } from 'ramda';
|
||||
import { assoc, assocPath, last, pipe, reject } from 'ramda';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { shortUrlMatches } from '../helpers';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
||||
|
@ -16,6 +16,8 @@ export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR
|
|||
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
||||
|
||||
export interface ShortUrlsList {
|
||||
shortUrls?: ShlinkShortUrlsResponse;
|
||||
loading: boolean;
|
||||
|
@ -75,10 +77,11 @@ export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
|||
),
|
||||
[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.
|
||||
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
|
||||
// We can also remove the items above the amount that is displayed there.
|
||||
(state: ShortUrlsList, { result }: CreateShortUrlAction) => !state.shortUrls ? state : assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
[ result, ...init(state.shortUrls.data) ],
|
||||
[ result, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1) ],
|
||||
state,
|
||||
),
|
||||
(state: ShortUrlsList) => !state.shortUrls ? state : assocPath(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { CreateVisit, Stats } from '../../visits/types';
|
|||
import { parseApiError } from '../../api/utils';
|
||||
import { TagStats } from '../data';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducers/shortUrlCreation';
|
||||
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||
|
||||
|
@ -42,6 +43,7 @@ interface FilterTagsAction extends Action<string> {
|
|||
type TagsCombinedAction = ListTagsAction
|
||||
& DeleteTagAction
|
||||
& CreateVisitsAction
|
||||
& CreateShortUrlAction
|
||||
& EditTagAction
|
||||
& FilterTagsAction
|
||||
& ApiErrorAction;
|
||||
|
@ -102,6 +104,10 @@ export default buildReducer<TagsList, TagsCombinedAction>({
|
|||
...state,
|
||||
stats: increaseVisitsForTags(calculateVisitsPerTag(createdVisits), state.stats),
|
||||
}),
|
||||
[CREATE_SHORT_URL]: ({ tags: stateTags, ...rest }, { result }) => ({
|
||||
...rest,
|
||||
tags: stateTags.concat(result.tags.filter((tag) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ]
|
||||
}),
|
||||
}, initialState);
|
||||
|
||||
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => () => async (
|
||||
|
|
|
@ -14,6 +14,8 @@ import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreat
|
|||
import { SHORT_URL_EDITED } from '../../../src/short-urls/reducers/shortUrlEdition';
|
||||
|
||||
describe('shortUrlsListReducer', () => {
|
||||
const shortCode = 'abc123';
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns loading on LIST_SHORT_URLS_START', () =>
|
||||
expect(reducer(undefined, { type: LIST_SHORT_URLS_START } as any)).toEqual({
|
||||
|
@ -35,7 +37,6 @@ describe('shortUrlsListReducer', () => {
|
|||
}));
|
||||
|
||||
it('removes matching URL and reduces total on SHORT_URL_DELETED', () => {
|
||||
const shortCode = 'abc123';
|
||||
const state = {
|
||||
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||
data: [
|
||||
|
@ -72,7 +73,6 @@ describe('shortUrlsListReducer', () => {
|
|||
[[{}], 10 ],
|
||||
[[], 10 ],
|
||||
])('updates visits count on CREATE_VISITS', (createdVisits, expectedCount) => {
|
||||
const shortCode = 'abc123';
|
||||
const state = {
|
||||
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
|
||||
data: [
|
||||
|
@ -98,16 +98,42 @@ describe('shortUrlsListReducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('prepends new short URL and increases total on CREATE_SHORT_URL', () => {
|
||||
it.each([
|
||||
[
|
||||
[
|
||||
Mock.of<ShortUrl>({ shortCode }),
|
||||
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||
],
|
||||
[{ shortCode: 'newOne' }, { shortCode }, { shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
||||
],
|
||||
[
|
||||
[
|
||||
Mock.of<ShortUrl>({ shortCode }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'code' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'bar' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'baz' }),
|
||||
],
|
||||
[{ shortCode: 'newOne' }, { shortCode }, { shortCode: 'code' }, { shortCode: 'foo' }, { shortCode: 'bar' }],
|
||||
],
|
||||
[
|
||||
[
|
||||
Mock.of<ShortUrl>({ shortCode }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'code' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'foo' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'bar' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'baz1' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'baz2' }),
|
||||
Mock.of<ShortUrl>({ shortCode: 'baz3' }),
|
||||
],
|
||||
[{ shortCode: 'newOne' }, { shortCode }, { shortCode: 'code' }, { shortCode: 'foo' }, { shortCode: 'bar' }],
|
||||
],
|
||||
])('prepends new short URL and increases total on CREATE_SHORT_URL', (data, expectedData) => {
|
||||
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' }),
|
||||
],
|
||||
data,
|
||||
pagination: Mock.of<ShlinkPaginator>({
|
||||
totalItems: 15,
|
||||
}),
|
||||
|
@ -118,7 +144,7 @@ describe('shortUrlsListReducer', () => {
|
|||
|
||||
expect(reducer(state, { type: CREATE_SHORT_URL, result: newShortUrl } as any)).toEqual({
|
||||
shortUrls: {
|
||||
data: [{ shortCode: 'newOne' }, { shortCode }, { shortCode, domain: 'example.com' }],
|
||||
data: expectedData,
|
||||
pagination: { totalItems: 16 },
|
||||
},
|
||||
loading: false,
|
||||
|
|
|
@ -11,6 +11,8 @@ import reducer, {
|
|||
import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete';
|
||||
import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit';
|
||||
import { ShlinkState } from '../../../src/container/types';
|
||||
import { ShortUrl } from '../../../src/short-urls/data';
|
||||
import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||
|
||||
describe('tagsListReducer', () => {
|
||||
const state = (props: Partial<TagsList>) => Mock.of<TagsList>(props);
|
||||
|
@ -74,6 +76,19 @@ describe('tagsListReducer', () => {
|
|||
filteredTags,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[[ 'foo', 'foo3', 'bar3', 'fo' ], [ 'foo', 'bar', 'baz', 'foo2', 'fo', 'foo3', 'bar3' ]],
|
||||
[[ 'foo', 'bar' ], [ 'foo', 'bar', 'baz', 'foo2', 'fo' ]],
|
||||
[[ 'new', 'tag' ], [ 'foo', 'bar', 'baz', 'foo2', 'fo', 'new', 'tag' ]],
|
||||
])('appends new short URL\'s tags to the list of tags on CREATE_SHORT_URL', (shortUrlTags, expectedTags) => {
|
||||
const tags = [ 'foo', 'bar', 'baz', 'foo2', 'fo' ];
|
||||
const result = Mock.of<ShortUrl>({ tags: shortUrlTags });
|
||||
|
||||
expect(reducer(state({ tags }), { type: CREATE_SHORT_URL, result } as any)).toEqual({
|
||||
tags: expectedTags,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterTags', () => {
|
||||
|
|
Loading…
Reference in a new issue