Merge pull request #563 from shlinkio/develop

Release 3.5.1
This commit is contained in:
Alejandro Celaya 2022-01-08 11:25:22 +01:00 committed by GitHub
commit e9fcdcb049
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 103 additions and 18 deletions

View file

@ -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".

View file

@ -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).

View file

@ -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;

View file

@ -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}>

View file

@ -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();
}, []);

View file

@ -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(

View file

@ -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 (

View file

@ -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,

View file

@ -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', () => {