mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-24 18:56:39 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da54a72b3e | ||
|
|
86c155d8d1 | ||
|
|
666d2d3065 | ||
|
|
01e69fb6ca | ||
|
|
30e5253acd | ||
|
|
c67ce3918b | ||
|
|
58077f2d86 | ||
|
|
098c94bccf | ||
|
|
861a3c068f | ||
|
|
3b95e8ebc0 | ||
|
|
170e427530 | ||
|
|
707c9f4ce6 | ||
|
|
dc672bf0f0 | ||
|
|
c682737505 | ||
|
|
46fa3d4345 | ||
|
|
9b7bc4b495 | ||
|
|
4385061499 | ||
|
|
e17498e68b | ||
|
|
3e298f010b | ||
|
|
30117bd121 | ||
|
|
93f33b6218 | ||
|
|
535d08a607 | ||
|
|
6ac3a49db2 | ||
|
|
c16f760d79 | ||
|
|
965c2b243f | ||
|
|
703addddb9 | ||
|
|
ab6dff5c31 | ||
|
|
2ef330c62b | ||
|
|
72e71aff40 | ||
|
|
cefd6ec752 | ||
|
|
aec3de18aa | ||
|
|
97620cb583 | ||
|
|
cf4e8190a4 | ||
|
|
8af7436f13 | ||
|
|
c53520ae56 | ||
|
|
3adcaef455 | ||
|
|
43cd9722a9 | ||
|
|
f3154e770e |
@@ -1,6 +1,6 @@
|
||||
build:
|
||||
environment:
|
||||
node: v12.11.0
|
||||
node: v12.14.1
|
||||
tools:
|
||||
external_code_coverage:
|
||||
timeout: 1200
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "12.11.0"
|
||||
- "12.14.1"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -4,6 +4,33 @@ 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).
|
||||
|
||||
## 2.3.1 - 2020-02-08
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#191](https://github.com/shlinkio/shlink-web-client/issues/191) Created `ForServerVersion` helper component which dynamically renders children if current server conditions are met.
|
||||
* [#189](https://github.com/shlinkio/shlink-web-client/issues/189) Simplified short url tags and short url deletion components and reducers, by removing redundant actions.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#193](https://github.com/shlinkio/shlink-web-client/issues/193) Fixed `maxVisits` being set to 0 when trying to reset it from having a value to `null`.
|
||||
* [#196](https://github.com/shlinkio/shlink-web-client/issues/196) Included apache `.htaccess` file which takes care of falling back to index.html when reloading the page on a client-side handled route.
|
||||
* [#179](https://github.com/shlinkio/shlink-web-client/issues/179) Ensured domain is provided to Shlink server when editing, deleting or fetching short URLs which do not belong to default domain.
|
||||
* [#202](https://github.com/shlinkio/shlink-web-client/issues/202) Fixed domain not passed when dispatching actions that affect a single short URL (edit tags, edit meta and delete), which cased the list not to be properly updated.
|
||||
|
||||
|
||||
## 2.3.0 - 2020-01-19
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12.11.1-alpine as node
|
||||
FROM node:12.14.1-alpine as node
|
||||
COPY . /shlink-web-client
|
||||
RUN cd /shlink-web-client && npm install && npm run build
|
||||
|
||||
|
||||
25
README.md
25
README.md
@@ -14,23 +14,30 @@ A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
||||
|
||||
There are three ways in which you can use this application.
|
||||
|
||||
* The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
|
||||
### From app.shlink.io
|
||||
|
||||
The application runs 100% in the browser, so you can safely access any shlink instance from there.
|
||||
The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
|
||||
|
||||
* Self hosting the application yourself.
|
||||
The application runs 100% in the browser, so you can safely access any shlink instance from there.
|
||||
|
||||
Get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
|
||||
### Docker image
|
||||
|
||||
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
|
||||
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the [shlinkio/shlink-web-client](https://hub.docker.com/r/shlinkio/shlink-web-client/) image and do it.
|
||||
|
||||
Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
|
||||
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
|
||||
|
||||
* Using the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
### Self-hosted
|
||||
|
||||
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the `shlinkio/shlink-web-client` image and do it.
|
||||
If you want to self-host it yourself, get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
|
||||
|
||||
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
|
||||
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
|
||||
|
||||
**Considerations**:
|
||||
|
||||
* Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
|
||||
* The app has a client-side router that handles dynamic paths. Because of that, you need to configure your web server to fall-back to the `index.html` file when requested files do not exist.
|
||||
* If you use Apache, you are covered, since the project includes an `.htaccess` file which already does this.
|
||||
* If you use nginx, you can [see how it's done](config/docker/nginx.conf) for the docker image and do the same.
|
||||
|
||||
## Pre-configuring servers
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ server {
|
||||
index index.html;
|
||||
|
||||
# When requesting static paths with extension, try them, and return a 404 if not found
|
||||
location ~ .+\.(css|js|html|png|jpg|jpeg|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
|
||||
location ~* .+\.(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ version: '3'
|
||||
services:
|
||||
shlink_web_client_node:
|
||||
container_name: shlink_web_client_node
|
||||
image: node:12.11.0-alpine
|
||||
image: node:12.14.1-alpine
|
||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
|
||||
16
public/.htaccess
Normal file
16
public/.htaccess
Normal file
@@ -0,0 +1,16 @@
|
||||
RewriteEngine on
|
||||
RewriteBase /
|
||||
|
||||
# do not do anything for already existing files
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule (.*) - [L]
|
||||
|
||||
# if request is no valid file NOR directory
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
# if static asset do not do anything
|
||||
RewriteRule (.*)(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) - [NC,L,R=404]
|
||||
# everything else should be redirected to /index.html so it can be routed by it
|
||||
RewriteRule (.*) /index.html [L]
|
||||
@@ -20,7 +20,7 @@ const mapActionService = (map, actionName) => ({
|
||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||
[actionName]: lazyService(container, actionName),
|
||||
});
|
||||
const connect = (propsFromState, actionServiceNames) =>
|
||||
const connect = (propsFromState, actionServiceNames = []) =>
|
||||
reduxConnect(
|
||||
propsFromState ? pick(propsFromState) : null,
|
||||
actionServiceNames.reduce(mapActionService, {})
|
||||
|
||||
@@ -63,3 +63,11 @@ body,
|
||||
.indivisible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background-color: $mainColor;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($mainColor, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
31
src/servers/helpers/ForServerVersion.js
Normal file
31
src/servers/helpers/ForServerVersion.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compareVersions } from '../../utils/utils';
|
||||
import { serverType } from '../prop-types';
|
||||
|
||||
const propTypes = {
|
||||
minVersion: PropTypes.string,
|
||||
maxVersion: PropTypes.string,
|
||||
selectedServer: serverType,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => {
|
||||
if (!selectedServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { version } = selectedServer;
|
||||
const matchesMinVersion = !minVersion || compareVersions(version, '>=', minVersion);
|
||||
const matchesMaxVersion = !maxVersion || compareVersions(version, '<=', maxVersion);
|
||||
|
||||
if (!matchesMinVersion || !matchesMaxVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
};
|
||||
|
||||
ForServerVersion.propTypes = propTypes;
|
||||
|
||||
export default ForServerVersion;
|
||||
@@ -5,4 +5,5 @@ export const serverType = PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
apiKey: PropTypes.string,
|
||||
version: PropTypes.string,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import DeleteServerButton from '../DeleteServerButton';
|
||||
import ImportServersBtn from '../helpers/ImportServersBtn';
|
||||
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
|
||||
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
|
||||
import ForServerVersion from '../helpers/ForServerVersion';
|
||||
import ServersImporter from './ServersImporter';
|
||||
import ServersService from './ServersService';
|
||||
import ServersExporter from './ServersExporter';
|
||||
@@ -28,6 +29,9 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
|
||||
bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ]));
|
||||
|
||||
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
|
||||
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
|
||||
|
||||
// Services
|
||||
bottle.constant('csvjson', csvjson);
|
||||
bottle.service('ServersImporter', ServersImporter, 'csvjson');
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Collapse, FormGroup, Input } from 'reactstrap';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import DateInput from '../utils/DateInput';
|
||||
import Checkbox from '../utils/Checkbox';
|
||||
import ForVersion from '../utils/ForVersion';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { compareVersions } from '../utils/utils';
|
||||
import { createShortUrlResultType } from './reducers/shortUrlCreation';
|
||||
@@ -15,7 +14,11 @@ import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||
const formatDate = (date) => isNil(date) ? date : date.format();
|
||||
|
||||
const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShortUrl extends React.Component {
|
||||
const CreateShortUrl = (
|
||||
TagsSelector,
|
||||
CreateShortUrlResult,
|
||||
ForServerVersion
|
||||
) => class CreateShortUrl extends React.Component {
|
||||
static propTypes = {
|
||||
createShortUrl: PropTypes.func,
|
||||
shortUrlCreationResult: createShortUrlResultType,
|
||||
@@ -116,7 +119,7 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ForVersion minVersion="1.16.0" currentServerVersion={currentServerVersion}>
|
||||
<ForServerVersion minVersion="1.16.0">
|
||||
<div className="mb-4 text-right">
|
||||
<Checkbox
|
||||
className="mr-2"
|
||||
@@ -127,7 +130,7 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort
|
||||
</Checkbox>
|
||||
<UseExistingIfFoundInfoIcon />
|
||||
</div>
|
||||
</ForVersion>
|
||||
</ForServerVersion>
|
||||
</Collapse>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -7,23 +7,19 @@ import moment from 'moment';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import Tag from '../tags/helpers/Tag';
|
||||
import DateRangeRow from '../utils/DateRangeRow';
|
||||
import { compareVersions, formatDate } from '../utils/utils';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { formatDate } from '../utils/utils';
|
||||
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||
import './SearchBar.scss';
|
||||
|
||||
const propTypes = {
|
||||
listShortUrls: PropTypes.func,
|
||||
shortUrlsListParams: shortUrlsListParamsType,
|
||||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const dateOrUndefined = (date) => date ? moment(date) : undefined;
|
||||
|
||||
const SearchBar = (colorGenerator) => {
|
||||
const SearchBar = ({ listShortUrls, shortUrlsListParams, selectedServer }) => {
|
||||
const currentServerVersion = selectedServer ? selectedServer.version : '';
|
||||
const enableDateFiltering = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '>=', '1.21.0');
|
||||
const SearchBar = (colorGenerator, ForServerVersion) => {
|
||||
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
|
||||
const selectedTags = shortUrlsListParams.tags || [];
|
||||
const setDate = (dateName) => pipe(
|
||||
formatDate(),
|
||||
@@ -38,7 +34,7 @@ const SearchBar = (colorGenerator) => {
|
||||
}
|
||||
/>
|
||||
|
||||
{enableDateFiltering && (
|
||||
<ForServerVersion minVersion="1.21.0">
|
||||
<div className="mt-3">
|
||||
<DateRangeRow
|
||||
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
|
||||
@@ -47,7 +43,7 @@ const SearchBar = (colorGenerator) => {
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ForServerVersion>
|
||||
|
||||
{!isEmpty(selectedTags) && (
|
||||
<h4 className="search-bar__selected-tag mt-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { identity } from 'ramda';
|
||||
import { identity, pipe } from 'ramda';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
import { shortUrlDeletionType } from '../reducers/shortUrlDeletion';
|
||||
|
||||
@@ -15,21 +15,17 @@ export default class DeleteShortUrlModal extends React.Component {
|
||||
shortUrlDeletion: shortUrlDeletionType,
|
||||
deleteShortUrl: PropTypes.func,
|
||||
resetDeleteShortUrl: PropTypes.func,
|
||||
shortUrlDeleted: PropTypes.func,
|
||||
};
|
||||
|
||||
state = { inputValue: '' };
|
||||
handleDeleteUrl = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { deleteShortUrl, shortUrl, toggle, shortUrlDeleted } = this.props;
|
||||
const { shortCode } = shortUrl;
|
||||
const { deleteShortUrl, shortUrl, toggle } = this.props;
|
||||
const { shortCode, domain } = shortUrl;
|
||||
|
||||
deleteShortUrl(shortCode)
|
||||
.then(() => {
|
||||
shortUrlDeleted(shortCode);
|
||||
toggle();
|
||||
})
|
||||
deleteShortUrl(shortCode, domain)
|
||||
.then(toggle)
|
||||
.catch(identity);
|
||||
};
|
||||
|
||||
@@ -40,16 +36,17 @@ export default class DeleteShortUrlModal extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { shortUrl, toggle, isOpen, shortUrlDeletion } = this.props;
|
||||
const { shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl } = this.props;
|
||||
const { error, errorData } = shortUrlDeletion;
|
||||
const errorCode = error && (errorData.type || errorData.error);
|
||||
const hasThresholdError = errorCode === THRESHOLD_REACHED;
|
||||
const hasErrorOtherThanThreshold = error && errorCode !== THRESHOLD_REACHED;
|
||||
const close = pipe(resetDeleteShortUrl, toggle);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<Modal isOpen={isOpen} toggle={close} centered>
|
||||
<form onSubmit={this.handleDeleteUrl}>
|
||||
<ModalHeader toggle={toggle}>
|
||||
<ModalHeader toggle={close}>
|
||||
<span className="text-danger">Delete short URL</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
@@ -77,7 +74,7 @@ export default class DeleteShortUrlModal extends React.Component {
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||
<button type="button" className="btn btn-link" onClick={close}>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-danger"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import moment from 'moment';
|
||||
import { pipe } from 'ramda';
|
||||
import { isEmpty, pipe } from 'ramda';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
import { shortUrlEditMetaType } from '../reducers/shortUrlMeta';
|
||||
import DateInput from '../../utils/DateInput';
|
||||
@@ -36,8 +36,8 @@ const EditMetaModal = (
|
||||
const [ maxVisits, setMaxVisits ] = useState(shortUrl && shortUrl.meta && shortUrl.meta.maxVisits);
|
||||
|
||||
const close = pipe(resetShortUrlMeta, toggle);
|
||||
const doEdit = () => editShortUrlMeta(shortUrl.shortCode, {
|
||||
maxVisits: maxVisits && parseInt(maxVisits),
|
||||
const doEdit = () => editShortUrlMeta(shortUrl.shortCode, shortUrl.domain, {
|
||||
maxVisits: maxVisits && !isEmpty(maxVisits) ? parseInt(maxVisits) : null,
|
||||
validSince: validSince && formatIsoDate(validSince),
|
||||
validUntil: validUntil && formatIsoDate(validUntil),
|
||||
}).then(close);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import { pipe } from 'ramda';
|
||||
import { shortUrlTagsType } from '../reducers/shortUrlTags';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
|
||||
@@ -12,36 +13,21 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon
|
||||
shortUrl: shortUrlType.isRequired,
|
||||
shortUrlTags: shortUrlTagsType,
|
||||
editShortUrlTags: PropTypes.func,
|
||||
shortUrlTagsEdited: PropTypes.func,
|
||||
resetShortUrlsTags: PropTypes.func,
|
||||
};
|
||||
|
||||
saveTags = () => {
|
||||
const { editShortUrlTags, shortUrl, toggle } = this.props;
|
||||
|
||||
editShortUrlTags(shortUrl.shortCode, this.state.tags)
|
||||
.then(() => {
|
||||
this.tagsSaved = true;
|
||||
toggle();
|
||||
})
|
||||
editShortUrlTags(shortUrl.shortCode, shortUrl.domain, this.state.tags)
|
||||
.then(toggle)
|
||||
.catch(() => {});
|
||||
};
|
||||
refreshShortUrls = () => {
|
||||
if (!this.tagsSaved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { shortUrlTagsEdited, shortUrl, shortUrlTags } = this.props;
|
||||
const { tags } = shortUrlTags;
|
||||
|
||||
shortUrlTagsEdited(shortUrl.shortCode, tags);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { resetShortUrlsTags } = this.props;
|
||||
|
||||
resetShortUrlsTags();
|
||||
this.tagsSaved = false;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -50,12 +36,13 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, toggle, shortUrl, shortUrlTags } = this.props;
|
||||
const { isOpen, toggle, shortUrl, shortUrlTags, resetShortUrlsTags } = this.props;
|
||||
const url = shortUrl && (shortUrl.shortUrl || '');
|
||||
const close = pipe(resetShortUrlsTags, toggle);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={() => this.refreshShortUrls()}>
|
||||
<ModalHeader toggle={toggle}>
|
||||
<Modal isOpen={isOpen} toggle={close} centered>
|
||||
<ModalHeader toggle={close}>
|
||||
Edit tags for <ExternalLink href={url} />
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
@@ -67,7 +54,7 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||
<button className="btn btn-link" onClick={close}>Cancel</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
|
||||
@@ -3,25 +3,33 @@ import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { shortUrlMetaType } from '../reducers/shortUrlMeta';
|
||||
import { serverType } from '../../servers/prop-types';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
import './ShortUrlVisitsCount.scss';
|
||||
import VisitStatsLink from './VisitStatsLink';
|
||||
|
||||
const propTypes = {
|
||||
visitsCount: PropTypes.number.isRequired,
|
||||
meta: shortUrlMetaType,
|
||||
shortUrl: shortUrlType,
|
||||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const ShortUrlVisitsCount = ({ visitsCount, meta }) => {
|
||||
const maxVisits = meta && meta.maxVisits;
|
||||
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer }) => {
|
||||
const maxVisits = shortUrl && shortUrl.meta && shortUrl.meta.maxVisits;
|
||||
const visitsLink = (
|
||||
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
||||
<strong>{visitsCount}</strong>
|
||||
</VisitStatsLink>
|
||||
);
|
||||
|
||||
if (!maxVisits) {
|
||||
return <span>{visitsCount}</span>;
|
||||
return visitsLink;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="indivisible">
|
||||
{visitsCount}
|
||||
{visitsLink}
|
||||
<small id="maxVisitsControl" className="short-urls-visits-count__max-visits-control">
|
||||
{' '}/ {maxVisits}{' '}
|
||||
<sup>
|
||||
|
||||
@@ -58,7 +58,11 @@ const ShortUrlsRow = (
|
||||
</td>
|
||||
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
|
||||
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">
|
||||
<ShortUrlVisitsCount visitsCount={shortUrl.visitsCount} meta={shortUrl.meta} />
|
||||
<ShortUrlVisitsCount
|
||||
visitsCount={shortUrl.visitsCount}
|
||||
shortUrl={shortUrl}
|
||||
selectedServer={selectedServer}
|
||||
/>
|
||||
</td>
|
||||
<td className="short-urls-row__cell short-urls-row__cell--relative">
|
||||
<small
|
||||
|
||||
@@ -10,21 +10,20 @@ import {
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty } from 'ramda';
|
||||
import { serverType } from '../../servers/prop-types';
|
||||
import { compareVersions } from '../../utils/utils';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
import PreviewModal from './PreviewModal';
|
||||
import QrCodeModal from './QrCodeModal';
|
||||
import VisitStatsLink from './VisitStatsLink';
|
||||
import './ShortUrlsRowMenu.scss';
|
||||
|
||||
const ShortUrlsRowMenu = (
|
||||
DeleteShortUrlModal,
|
||||
EditTagsModal,
|
||||
EditMetaModal
|
||||
EditMetaModal,
|
||||
ForServerVersion
|
||||
) => class ShortUrlsRowMenu extends React.Component {
|
||||
static propTypes = {
|
||||
onCopyToClipboard: PropTypes.func,
|
||||
@@ -45,9 +44,6 @@ const ShortUrlsRowMenu = (
|
||||
render() {
|
||||
const { onCopyToClipboard, shortUrl, selectedServer } = this.props;
|
||||
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
||||
const currentServerVersion = selectedServer ? selectedServer.version : '';
|
||||
const showEditMetaBtn = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '>=', '1.18.0');
|
||||
const showPreviewBtn = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '<', '2.0.0');
|
||||
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
|
||||
const toggleQrCode = toggleModal('isQrModalOpen');
|
||||
const togglePreview = toggleModal('isPreviewModalOpen');
|
||||
@@ -61,7 +57,7 @@ const ShortUrlsRowMenu = (
|
||||
<FontAwesomeIcon icon={menuIcon} />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right>
|
||||
<DropdownItem tag={Link} to={`/server/${selectedServer ? selectedServer.id : ''}/short-code/${shortUrl.shortCode}/visits`}>
|
||||
<DropdownItem tag={VisitStatsLink} selectedServer={selectedServer} shortUrl={shortUrl}>
|
||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||
</DropdownItem>
|
||||
|
||||
@@ -70,14 +66,12 @@ const ShortUrlsRowMenu = (
|
||||
</DropdownItem>
|
||||
<EditTagsModal shortUrl={shortUrl} isOpen={this.state.isTagsModalOpen} toggle={toggleTags} />
|
||||
|
||||
{showEditMetaBtn && (
|
||||
<React.Fragment>
|
||||
<DropdownItem onClick={toggleMeta}>
|
||||
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit metadata
|
||||
</DropdownItem>
|
||||
<EditMetaModal shortUrl={shortUrl} isOpen={this.state.isMetaModalOpen} toggle={toggleMeta} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<ForServerVersion minVersion="1.18.0">
|
||||
<DropdownItem onClick={toggleMeta}>
|
||||
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit metadata
|
||||
</DropdownItem>
|
||||
<EditMetaModal shortUrl={shortUrl} isOpen={this.state.isMetaModalOpen} toggle={toggleMeta} />
|
||||
</ForServerVersion>
|
||||
|
||||
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
|
||||
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Delete short URL
|
||||
@@ -86,21 +80,21 @@ const ShortUrlsRowMenu = (
|
||||
|
||||
<DropdownItem divider />
|
||||
|
||||
{showPreviewBtn && (
|
||||
<React.Fragment>
|
||||
<DropdownItem onClick={togglePreview}>
|
||||
<FontAwesomeIcon icon={pictureIcon} fixedWidth /> Preview
|
||||
</DropdownItem>
|
||||
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<ForServerVersion maxVersion="1.x">
|
||||
<DropdownItem onClick={togglePreview}>
|
||||
<FontAwesomeIcon icon={pictureIcon} fixedWidth /> Preview
|
||||
</DropdownItem>
|
||||
<PreviewModal url={completeShortUrl} isOpen={this.state.isPreviewModalOpen} toggle={togglePreview} />
|
||||
</ForServerVersion>
|
||||
|
||||
<DropdownItem onClick={toggleQrCode}>
|
||||
<FontAwesomeIcon icon={qrIcon} fixedWidth /> QR code
|
||||
</DropdownItem>
|
||||
<QrCodeModal url={completeShortUrl} isOpen={this.state.isQrModalOpen} toggle={toggleQrCode} />
|
||||
|
||||
{showPreviewBtn && <DropdownItem divider />}
|
||||
<ForServerVersion maxVersion="1.x">
|
||||
<DropdownItem divider />
|
||||
</ForServerVersion>
|
||||
|
||||
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
|
||||
<DropdownItem>
|
||||
|
||||
29
src/short-urls/helpers/VisitStatsLink.js
Normal file
29
src/short-urls/helpers/VisitStatsLink.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { serverType } from '../../servers/prop-types';
|
||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||
|
||||
const propTypes = {
|
||||
shortUrl: shortUrlType,
|
||||
selectedServer: serverType,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const buildVisitsUrl = ({ id }, { shortCode, domain }) => {
|
||||
const query = domain ? `?domain=${domain}` : '';
|
||||
|
||||
return `/server/${id}/short-code/${shortCode}/visits${query}`;
|
||||
};
|
||||
|
||||
const VisitStatsLink = ({ selectedServer, shortUrl, children, ...rest }) => {
|
||||
if (!selectedServer || !shortUrl) {
|
||||
return <span {...rest}>{children}</span>;
|
||||
}
|
||||
|
||||
return <Link to={buildVisitsUrl(selectedServer, shortUrl)} {...rest}>{children}</Link>;
|
||||
};
|
||||
|
||||
VisitStatsLink.propTypes = propTypes;
|
||||
|
||||
export default VisitStatsLink;
|
||||
@@ -5,9 +5,8 @@ import { apiErrorType } from '../../utils/services/ShlinkApiClient';
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
||||
export const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR';
|
||||
export const DELETE_SHORT_URL = 'shlink/deleteShortUrl/DELETE_SHORT_URL';
|
||||
export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
|
||||
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
|
||||
export const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const shortUrlDeletionType = PropTypes.shape({
|
||||
@@ -27,18 +26,18 @@ const initialState = {
|
||||
export default handleActions({
|
||||
[DELETE_SHORT_URL_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[DELETE_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, errorData, loading: false, error: true }),
|
||||
[DELETE_SHORT_URL]: (state, { shortCode }) => ({ ...state, shortCode, loading: false, error: false }),
|
||||
[SHORT_URL_DELETED]: (state, { shortCode }) => ({ ...state, shortCode, loading: false, error: false }),
|
||||
[RESET_DELETE_SHORT_URL]: () => initialState,
|
||||
}, initialState);
|
||||
|
||||
export const deleteShortUrl = (buildShlinkApiClient) => (shortCode) => async (dispatch, getState) => {
|
||||
export const deleteShortUrl = (buildShlinkApiClient) => (shortCode, domain) => async (dispatch, getState) => {
|
||||
dispatch({ type: DELETE_SHORT_URL_START });
|
||||
|
||||
const { deleteShortUrl } = await buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
await deleteShortUrl(shortCode);
|
||||
dispatch({ type: DELETE_SHORT_URL, shortCode });
|
||||
await deleteShortUrl(shortCode, domain);
|
||||
dispatch({ type: SHORT_URL_DELETED, shortCode, domain });
|
||||
} catch (e) {
|
||||
dispatch({ type: DELETE_SHORT_URL_ERROR, errorData: e.response.data });
|
||||
|
||||
@@ -47,5 +46,3 @@ export const deleteShortUrl = (buildShlinkApiClient) => (shortCode) => async (di
|
||||
};
|
||||
|
||||
export const resetDeleteShortUrl = createAction(RESET_DELETE_SHORT_URL);
|
||||
|
||||
export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode });
|
||||
|
||||
@@ -35,13 +35,13 @@ export default handleActions({
|
||||
[RESET_EDIT_SHORT_URL_META]: () => initialState,
|
||||
}, initialState);
|
||||
|
||||
export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => async (dispatch, getState) => {
|
||||
export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, domain, meta) => async (dispatch, getState) => {
|
||||
dispatch({ type: EDIT_SHORT_URL_META_START });
|
||||
const { updateShortUrlMeta } = await buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
await updateShortUrlMeta(shortCode, meta);
|
||||
dispatch({ shortCode, meta, type: SHORT_URL_META_EDITED });
|
||||
await updateShortUrlMeta(shortCode, domain, meta);
|
||||
dispatch({ shortCode, meta, domain, type: SHORT_URL_META_EDITED });
|
||||
} catch (e) {
|
||||
dispatch({ type: EDIT_SHORT_URL_META_ERROR });
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
|
||||
export const EDIT_SHORT_URL_TAGS_ERROR = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_ERROR';
|
||||
export const EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS';
|
||||
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
|
||||
export const SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED';
|
||||
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const shortUrlTagsType = PropTypes.shape({
|
||||
@@ -26,18 +25,18 @@ const initialState = {
|
||||
export default handleActions({
|
||||
[EDIT_SHORT_URL_TAGS_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||
[EDIT_SHORT_URL_TAGS_ERROR]: (state) => ({ ...state, saving: false, error: true }),
|
||||
[EDIT_SHORT_URL_TAGS]: (state, { shortCode, tags }) => ({ shortCode, tags, saving: false, error: false }),
|
||||
[SHORT_URL_TAGS_EDITED]: (state, { shortCode, tags }) => ({ shortCode, tags, saving: false, error: false }),
|
||||
[RESET_EDIT_SHORT_URL_TAGS]: () => initialState,
|
||||
}, initialState);
|
||||
|
||||
export const editShortUrlTags = (buildShlinkApiClient) => (shortCode, tags) => async (dispatch, getState) => {
|
||||
export const editShortUrlTags = (buildShlinkApiClient) => (shortCode, domain, tags) => async (dispatch, getState) => {
|
||||
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
|
||||
const { updateShortUrlTags } = await buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const normalizedTags = await updateShortUrlTags(shortCode, tags);
|
||||
const normalizedTags = await updateShortUrlTags(shortCode, domain, tags);
|
||||
|
||||
dispatch({ tags: normalizedTags, shortCode, type: EDIT_SHORT_URL_TAGS });
|
||||
dispatch({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED });
|
||||
} catch (e) {
|
||||
dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR });
|
||||
|
||||
@@ -46,9 +45,3 @@ export const editShortUrlTags = (buildShlinkApiClient) => (shortCode, tags) => a
|
||||
};
|
||||
|
||||
export const resetShortUrlsTags = createAction(RESET_EDIT_SHORT_URL_TAGS);
|
||||
|
||||
export const shortUrlTagsEdited = (shortCode, tags) => ({
|
||||
tags,
|
||||
shortCode,
|
||||
type: SHORT_URL_TAGS_EDITED,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import { assoc, assocPath, propEq, reject } from 'ramda';
|
||||
import { assoc, assocPath, isNil, reject } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||
@@ -18,6 +18,7 @@ export const shortUrlType = PropTypes.shape({
|
||||
visitsCount: PropTypes.number,
|
||||
meta: shortUrlMetaType,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
domain: PropTypes.string,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
@@ -26,10 +27,18 @@ const initialState = {
|
||||
error: false,
|
||||
};
|
||||
|
||||
const setPropFromActionOnMatchingShortUrl = (prop) => (state, { shortCode, [prop]: propValue }) => assocPath(
|
||||
const shortUrlMatches = (shortUrl, shortCode, domain) => {
|
||||
if (isNil(domain)) {
|
||||
return shortUrl.shortCode === shortCode && !shortUrl.domain;
|
||||
}
|
||||
|
||||
return shortUrl.shortCode === shortCode && shortUrl.domain === domain;
|
||||
};
|
||||
|
||||
const setPropFromActionOnMatchingShortUrl = (prop) => (state, { shortCode, domain, [prop]: propValue }) => assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
state.shortUrls.data.map(
|
||||
(shortUrl) => shortUrl.shortCode === shortCode ? assoc(prop, propValue, shortUrl) : shortUrl
|
||||
(shortUrl) => shortUrlMatches(shortUrl, shortCode, domain) ? assoc(prop, propValue, shortUrl) : shortUrl
|
||||
),
|
||||
state
|
||||
);
|
||||
@@ -38,9 +47,9 @@ export default handleActions({
|
||||
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[LIST_SHORT_URLS]: (state, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true, shortUrls: {} }),
|
||||
[SHORT_URL_DELETED]: (state, { shortCode }) => assocPath(
|
||||
[SHORT_URL_DELETED]: (state, { shortCode, domain }) => assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
reject(propEq('shortCode', shortCode), state.shortUrls.data),
|
||||
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
|
||||
state,
|
||||
),
|
||||
[SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'),
|
||||
|
||||
@@ -12,8 +12,8 @@ import EditMetaModal from '../helpers/EditMetaModal';
|
||||
import CreateShortUrlResult from '../helpers/CreateShortUrlResult';
|
||||
import { listShortUrls } from '../reducers/shortUrlsList';
|
||||
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
|
||||
import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion';
|
||||
import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../reducers/shortUrlTags';
|
||||
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
|
||||
import { editShortUrlTags, resetShortUrlsTags } from '../reducers/shortUrlTags';
|
||||
import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta';
|
||||
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
||||
|
||||
@@ -24,8 +24,8 @@ const provideServices = (bottle, connect) => {
|
||||
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
|
||||
));
|
||||
|
||||
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
|
||||
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams', 'selectedServer' ], [ 'listShortUrls' ]));
|
||||
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion');
|
||||
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
||||
|
||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
|
||||
bottle.decorator('ShortUrlsList', connect(
|
||||
@@ -35,26 +35,27 @@ const provideServices = (bottle, connect) => {
|
||||
|
||||
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'stateFlagTimeout');
|
||||
|
||||
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal', 'EditMetaModal');
|
||||
bottle.serviceFactory(
|
||||
'ShortUrlsRowMenu',
|
||||
ShortUrlsRowMenu,
|
||||
'DeleteShortUrlModal',
|
||||
'EditTagsModal',
|
||||
'EditMetaModal',
|
||||
'ForServerVersion'
|
||||
);
|
||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'stateFlagTimeout');
|
||||
|
||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult');
|
||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult', 'ForServerVersion');
|
||||
bottle.decorator(
|
||||
'CreateShortUrl',
|
||||
connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ])
|
||||
);
|
||||
|
||||
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
|
||||
bottle.decorator('DeleteShortUrlModal', connect(
|
||||
[ 'shortUrlDeletion' ],
|
||||
[ 'deleteShortUrl', 'resetDeleteShortUrl', 'shortUrlDeleted' ]
|
||||
));
|
||||
bottle.decorator('DeleteShortUrlModal', connect([ 'shortUrlDeletion' ], [ 'deleteShortUrl', 'resetDeleteShortUrl' ]));
|
||||
|
||||
bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector');
|
||||
bottle.decorator('EditTagsModal', connect(
|
||||
[ 'shortUrlTags' ],
|
||||
[ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ]
|
||||
));
|
||||
bottle.decorator('EditTagsModal', connect([ 'shortUrlTags' ], [ 'editShortUrlTags', 'resetShortUrlsTags' ]));
|
||||
|
||||
bottle.serviceFactory('EditMetaModal', () => EditMetaModal);
|
||||
bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta', 'resetShortUrlMeta' ]));
|
||||
@@ -62,7 +63,6 @@ const provideServices = (bottle, connect) => {
|
||||
// Actions
|
||||
bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags);
|
||||
bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited);
|
||||
|
||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams);
|
||||
@@ -72,7 +72,6 @@ const provideServices = (bottle, connect) => {
|
||||
|
||||
bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('resetDeleteShortUrl', () => resetDeleteShortUrl);
|
||||
bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted);
|
||||
|
||||
bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty } from 'ramda';
|
||||
import { compareVersions } from './utils';
|
||||
|
||||
const propTypes = {
|
||||
minVersion: PropTypes.string.isRequired,
|
||||
currentServerVersion: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const ForVersion = ({ minVersion, currentServerVersion, children }) =>
|
||||
isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', minVersion)
|
||||
? null
|
||||
: <React.Fragment>{children}</React.Fragment>;
|
||||
|
||||
ForVersion.propTypes = propTypes;
|
||||
|
||||
export default ForVersion;
|
||||
@@ -1,5 +1,5 @@
|
||||
import qs from 'qs';
|
||||
import { isEmpty, isNil, pipe, reject } from 'ramda';
|
||||
import { isEmpty, isNil, reject } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const apiErrorType = PropTypes.shape({
|
||||
@@ -12,6 +12,7 @@ export const apiErrorType = PropTypes.shape({
|
||||
});
|
||||
|
||||
const buildShlinkBaseUrl = (url, apiVersion) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||
const rejectNilProps = reject(isNil);
|
||||
|
||||
export default class ShlinkApiClient {
|
||||
constructor(axios, baseUrl, apiKey) {
|
||||
@@ -21,10 +22,8 @@ export default class ShlinkApiClient {
|
||||
this._apiKey = apiKey || '';
|
||||
}
|
||||
|
||||
listShortUrls = pipe(
|
||||
(options = {}) => reject(isNil, options),
|
||||
(options) => this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls)
|
||||
);
|
||||
listShortUrls = (options = {}) =>
|
||||
this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls);
|
||||
|
||||
createShortUrl = (options) => {
|
||||
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options);
|
||||
@@ -37,20 +36,20 @@ export default class ShlinkApiClient {
|
||||
this._performRequest(`/short-urls/${shortCode}/visits`, 'GET', query)
|
||||
.then((resp) => resp.data.visits);
|
||||
|
||||
getShortUrl = (shortCode) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'GET')
|
||||
getShortUrl = (shortCode, domain) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'GET', { domain })
|
||||
.then((resp) => resp.data);
|
||||
|
||||
deleteShortUrl = (shortCode) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'DELETE')
|
||||
deleteShortUrl = (shortCode, domain) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
|
||||
.then(() => ({}));
|
||||
|
||||
updateShortUrlTags = (shortCode, tags) =>
|
||||
this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', {}, { tags })
|
||||
updateShortUrlTags = (shortCode, domain, tags) =>
|
||||
this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
|
||||
.then((resp) => resp.data.tags);
|
||||
|
||||
updateShortUrlMeta = (shortCode, meta) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'PATCH', {}, meta)
|
||||
updateShortUrlMeta = (shortCode, domain, meta) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
|
||||
.then(() => meta);
|
||||
|
||||
listTags = () =>
|
||||
@@ -73,7 +72,7 @@ export default class ShlinkApiClient {
|
||||
method,
|
||||
url: `${buildShlinkBaseUrl(this._baseUrl, this._apiVersion)}${url}`,
|
||||
headers: { 'X-Api-Key': this._apiKey },
|
||||
params: query,
|
||||
params: rejectNilProps(query),
|
||||
data: body,
|
||||
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isEmpty, mapObjIndexed, values } from 'ramda';
|
||||
import React from 'react';
|
||||
import { Card } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import qs from 'qs';
|
||||
import DateRangeRow from '../utils/DateRangeRow';
|
||||
import MutedMessage from '../utils/MuttedMessage';
|
||||
import { formatDate } from '../utils/utils';
|
||||
@@ -21,6 +22,9 @@ const ShortUrlVisits = (
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
getShortUrlVisits: PropTypes.func,
|
||||
shortUrlVisits: shortUrlVisitsType,
|
||||
getShortUrlDetail: PropTypes.func,
|
||||
@@ -29,24 +33,24 @@ const ShortUrlVisits = (
|
||||
};
|
||||
|
||||
state = { startDate: undefined, endDate: undefined };
|
||||
loadVisits = () => {
|
||||
const { match: { params }, getShortUrlVisits } = this.props;
|
||||
loadVisits = (loadDetail = false) => {
|
||||
const { match: { params }, location: { search }, getShortUrlVisits, getShortUrlDetail } = this.props;
|
||||
const { shortCode } = params;
|
||||
const dates = mapObjIndexed(formatDate(), this.state);
|
||||
const { startDate, endDate } = dates;
|
||||
const { startDate, endDate } = mapObjIndexed(formatDate(), this.state);
|
||||
const { domain } = qs.parse(search, { ignoreQueryPrefix: true });
|
||||
|
||||
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calcs
|
||||
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations
|
||||
this.memoizationId = `${this.timeWhenMounted}_${shortCode}_${startDate}_${endDate}`;
|
||||
getShortUrlVisits(shortCode, dates);
|
||||
getShortUrlVisits(shortCode, { startDate, endDate, domain });
|
||||
|
||||
if (loadDetail) {
|
||||
getShortUrlDetail(shortCode, domain);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { match: { params }, getShortUrlDetail } = this.props;
|
||||
const { shortCode } = params;
|
||||
|
||||
this.timeWhenMounted = new Date().getTime();
|
||||
this.loadVisits();
|
||||
getShortUrlDetail(shortCode);
|
||||
this.loadVisits(true);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
|
||||
<h2>
|
||||
<span className="badge badge-main float-right">
|
||||
Visits:{' '}
|
||||
<ShortUrlVisitsCount visitsCount={visits.length} meta={shortUrl.meta} />
|
||||
<ShortUrlVisitsCount visitsCount={visits.length} shortUrl={shortUrl} />
|
||||
</span>
|
||||
Visit stats for <ExternalLink href={shortLink} />
|
||||
</h2>
|
||||
|
||||
@@ -26,13 +26,13 @@ export default handleActions({
|
||||
[GET_SHORT_URL_DETAIL]: (state, { shortUrl }) => ({ shortUrl, loading: false, error: false }),
|
||||
}, initialState);
|
||||
|
||||
export const getShortUrlDetail = (buildShlinkApiClient) => (shortCode) => async (dispatch, getState) => {
|
||||
export const getShortUrlDetail = (buildShlinkApiClient) => (shortCode, domain) => async (dispatch, getState) => {
|
||||
dispatch({ type: GET_SHORT_URL_DETAIL_START });
|
||||
|
||||
const { getShortUrl } = await buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const shortUrl = await getShortUrl(shortCode);
|
||||
const shortUrl = await getShortUrl(shortCode, domain);
|
||||
|
||||
dispatch({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,7 +49,7 @@ export default handleActions({
|
||||
[GET_SHORT_URL_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||
}, initialState);
|
||||
|
||||
export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) => async (dispatch, getState) => {
|
||||
export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, query) => async (dispatch, getState) => {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||
|
||||
const { getShortUrlVisits } = await buildShlinkApiClient(getState);
|
||||
@@ -57,7 +57,7 @@ export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) =>
|
||||
const isLastPage = ({ currentPage, pagesCount }) => currentPage >= pagesCount;
|
||||
|
||||
const loadVisits = async (page = 1) => {
|
||||
const { pagination, data } = await getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage });
|
||||
const { pagination, data } = await getShortUrlVisits(shortCode, { ...query, page, itemsPerPage });
|
||||
|
||||
// If pagination was not returned, then this is an older shlink version. Just return data
|
||||
if (!pagination || isLastPage(pagination)) {
|
||||
@@ -96,7 +96,7 @@ export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) =>
|
||||
const loadVisitsInParallel = (pages) =>
|
||||
Promise.all(pages.map(
|
||||
(page) =>
|
||||
getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage })
|
||||
getShortUrlVisits(shortCode, { ...query, page, itemsPerPage })
|
||||
.then(prop('data'))
|
||||
)).then(flatten);
|
||||
|
||||
|
||||
48
test/servers/helpers/ForServerVersion.test.js
Normal file
48
test/servers/helpers/ForServerVersion.test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import each from 'jest-each';
|
||||
import ForServerVersion from '../../../src/servers/helpers/ForServerVersion';
|
||||
|
||||
describe('<ForServerVersion />', () => {
|
||||
let wrapped;
|
||||
|
||||
const renderComponent = (minVersion, maxVersion, selectedServer) => {
|
||||
wrapped = mount(
|
||||
<ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}>
|
||||
<span>Hello</span>
|
||||
</ForServerVersion>
|
||||
);
|
||||
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
afterEach(() => wrapped && wrapped.unmount());
|
||||
|
||||
it('does not render children when current server is empty', () => {
|
||||
const wrapped = renderComponent('1');
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
||||
each([
|
||||
[ '2.0.0', undefined, '1.8.3' ],
|
||||
[ undefined, '1.8.0', '1.8.3' ],
|
||||
[ '1.7.0', '1.8.0', '1.8.3' ],
|
||||
]).it('does not render children when current version does not match requirements', (min, max, version) => {
|
||||
const wrapped = renderComponent(min, max, { version });
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
||||
each([
|
||||
[ '2.0.0', undefined, '2.8.3' ],
|
||||
[ '2.0.0', undefined, '2.0.0' ],
|
||||
[ undefined, '1.8.0', '1.8.0' ],
|
||||
[ undefined, '1.8.0', '1.7.1' ],
|
||||
[ '1.7.0', '1.8.0', '1.7.3' ],
|
||||
]).it('renders children when current version matches requirements', (min, max, version) => {
|
||||
const wrapped = renderComponent(min, max, { version });
|
||||
|
||||
expect(wrapped.html()).toContain('<span>Hello</span>');
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ describe('<CreateShortUrl />', () => {
|
||||
const createShortUrl = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '');
|
||||
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '', () => '');
|
||||
|
||||
wrapper = shallow(
|
||||
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
||||
|
||||
@@ -9,7 +9,7 @@ import DateRangeRow from '../../src/utils/DateRangeRow';
|
||||
describe('<SearchBar />', () => {
|
||||
let wrapper;
|
||||
const listShortUrlsMock = jest.fn();
|
||||
const SearchBar = searchBarCreator({});
|
||||
const SearchBar = searchBarCreator({}, () => '');
|
||||
|
||||
afterEach(() => {
|
||||
listShortUrlsMock.mockReset();
|
||||
@@ -22,15 +22,10 @@ describe('<SearchBar />', () => {
|
||||
expect(wrapper.find(SearchField)).toHaveLength(1);
|
||||
});
|
||||
|
||||
each([
|
||||
[ '2.0.0', 1 ],
|
||||
[ '1.21.2', 1 ],
|
||||
[ '1.21.0', 1 ],
|
||||
[ '1.20.0', 0 ],
|
||||
]).it('renders a DateRangeRow when proper version is run', (version, expectedLength) => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} selectedServer={{ version }} />);
|
||||
it('renders a DateRangeRow', () => {
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
|
||||
|
||||
expect(wrapper.find(DateRangeRow)).toHaveLength(expectedLength);
|
||||
expect(wrapper.find(DateRangeRow)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders no tags when the list of tags is empty', () => {
|
||||
@@ -69,7 +64,7 @@ describe('<SearchBar />', () => {
|
||||
|
||||
each([ 'startDateChange', 'endDateChange' ]).it('updates short URLs list when date range changes', (event) => {
|
||||
wrapper = shallow(
|
||||
<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} selectedServer={{ version: '2.0.0' }} />
|
||||
<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />
|
||||
);
|
||||
const dateRange = wrapper.find(DateRangeRow);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ describe('<DeleteShortUrlModal />', () => {
|
||||
toggle={identity}
|
||||
deleteShortUrl={deleteShortUrl}
|
||||
resetDeleteShortUrl={identity}
|
||||
shortUrlDeleted={identity}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Modal } from 'reactstrap';
|
||||
import each from 'jest-each';
|
||||
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
||||
|
||||
describe('<EditTagsModal />', () => {
|
||||
@@ -8,10 +9,9 @@ describe('<EditTagsModal />', () => {
|
||||
const shortCode = 'abc123';
|
||||
const TagsSelector = () => '';
|
||||
const editShortUrlTags = jest.fn(() => Promise.resolve());
|
||||
const shortUrlTagsEdited = jest.fn();
|
||||
const resetShortUrlsTags = jest.fn();
|
||||
const toggle = jest.fn();
|
||||
const createWrapper = (shortUrlTags) => {
|
||||
const createWrapper = (shortUrlTags, domain) => {
|
||||
const EditTagsModal = createEditTagsModal(TagsSelector);
|
||||
|
||||
wrapper = shallow(
|
||||
@@ -20,12 +20,12 @@ describe('<EditTagsModal />', () => {
|
||||
shortUrl={{
|
||||
tags: [],
|
||||
shortCode,
|
||||
domain,
|
||||
originalUrl: 'https://long-domain.com/foo/bar',
|
||||
}}
|
||||
shortUrlTags={shortUrlTags}
|
||||
toggle={toggle}
|
||||
editShortUrlTags={editShortUrlTags}
|
||||
shortUrlTagsEdited={shortUrlTagsEdited}
|
||||
resetShortUrlsTags={resetShortUrlsTags}
|
||||
/>
|
||||
);
|
||||
@@ -76,19 +76,19 @@ describe('<EditTagsModal />', () => {
|
||||
expect(saveBtn.text()).toEqual('Saving tags...');
|
||||
});
|
||||
|
||||
it('saves tags when save button is clicked', (done) => {
|
||||
each([[ undefined ], [ null ], [ 'example.com' ]]).it('saves tags when save button is clicked', (domain, done) => {
|
||||
const wrapper = createWrapper({
|
||||
shortCode,
|
||||
tags: [],
|
||||
saving: true,
|
||||
error: false,
|
||||
});
|
||||
}, domain);
|
||||
const saveBtn = wrapper.find('.btn-primary');
|
||||
|
||||
saveBtn.simulate('click');
|
||||
|
||||
expect(editShortUrlTags).toHaveBeenCalledTimes(1);
|
||||
expect(editShortUrlTags).toHaveBeenCalledWith(shortCode, []);
|
||||
expect(editShortUrlTags).toHaveBeenCalledWith(shortCode, domain, []);
|
||||
|
||||
// Wrap this expect in a setImmediate since it is called as a result of an inner promise
|
||||
setImmediate(() => {
|
||||
@@ -107,28 +107,7 @@ describe('<EditTagsModal />', () => {
|
||||
const modal = wrapper.find(Modal);
|
||||
|
||||
modal.simulate('closed');
|
||||
expect(shortUrlTagsEdited).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('notifies tags have been edited when window is closed after saving', (done) => {
|
||||
const wrapper = createWrapper({
|
||||
shortCode,
|
||||
tags: [],
|
||||
saving: true,
|
||||
error: false,
|
||||
});
|
||||
const saveBtn = wrapper.find('.btn-primary');
|
||||
const modal = wrapper.find(Modal);
|
||||
|
||||
saveBtn.simulate('click');
|
||||
|
||||
// Wrap this expect in a setImmediate since it is called as a result of an inner promise
|
||||
setImmediate(() => {
|
||||
modal.simulate('closed');
|
||||
expect(shortUrlTagsEdited).toHaveBeenCalledTimes(1);
|
||||
expect(shortUrlTagsEdited).toHaveBeenCalledWith(shortCode, []);
|
||||
done();
|
||||
});
|
||||
expect(editShortUrlTags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toggles modal when cancel button is clicked', () => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsC
|
||||
describe('<ShortUrlVisitsCount />', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (visitsCount, meta) => {
|
||||
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} meta={meta} />);
|
||||
const createWrapper = (visitsCount, shortUrl) => {
|
||||
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} shortUrl={shortUrl} />);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
@@ -17,11 +17,11 @@ describe('<ShortUrlVisitsCount />', () => {
|
||||
|
||||
each([ undefined, {}]).it('just returns visits when no maxVisits is provided', (meta) => {
|
||||
const visitsCount = 45;
|
||||
const wrapper = createWrapper(visitsCount, meta);
|
||||
const wrapper = createWrapper(visitsCount, { meta });
|
||||
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||
|
||||
expect(wrapper.html()).toEqual(`<span>${visitsCount}</span>`);
|
||||
expect(wrapper.html()).toEqual(`<span><strong>${visitsCount}</strong></span>`);
|
||||
expect(maxVisitsHelper).toHaveLength(0);
|
||||
expect(maxVisitsTooltip).toHaveLength(0);
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe('<ShortUrlVisitsCount />', () => {
|
||||
const visitsCount = 45;
|
||||
const maxVisits = 500;
|
||||
const meta = { maxVisits };
|
||||
const wrapper = createWrapper(visitsCount, meta);
|
||||
const wrapper = createWrapper(visitsCount, { meta });
|
||||
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
||||
import each from 'jest-each';
|
||||
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
||||
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||
@@ -17,12 +16,12 @@ describe('<ShortUrlsRowMenu />', () => {
|
||||
shortCode: 'abc123',
|
||||
shortUrl: 'https://doma.in/abc123',
|
||||
};
|
||||
const createWrapper = (serverVersion = '1.21.1') => {
|
||||
const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal, EditMetaModal);
|
||||
const createWrapper = () => {
|
||||
const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal, EditMetaModal, () => '');
|
||||
|
||||
wrapper = shallow(
|
||||
<ShortUrlsRowMenu
|
||||
selectedServer={{ ...selectedServer, version: serverVersion }}
|
||||
selectedServer={selectedServer}
|
||||
shortUrl={shortUrl}
|
||||
onCopyToClipboard={onCopyToClipboard}
|
||||
/>
|
||||
@@ -46,24 +45,12 @@ describe('<ShortUrlsRowMenu />', () => {
|
||||
expect(qrCodeModal).toHaveLength(1);
|
||||
});
|
||||
|
||||
each([
|
||||
[ '1.17.0', 6, 2 ],
|
||||
[ '1.17.2', 6, 2 ],
|
||||
[ '1.18.0', 7, 2 ],
|
||||
[ '1.18.1', 7, 2 ],
|
||||
[ '1.19.0', 7, 2 ],
|
||||
[ '1.20.3', 7, 2 ],
|
||||
[ '1.21.0', 7, 2 ],
|
||||
[ '1.21.1', 7, 2 ],
|
||||
[ '2.0.0', 6, 1 ],
|
||||
[ '2.0.1', 6, 1 ],
|
||||
[ '2.1.0', 6, 1 ],
|
||||
]).it('renders correct amount of menu items depending on the version', (version, expectedNonDividerItems, expectedDividerItems) => {
|
||||
const wrapper = createWrapper(version);
|
||||
it('renders correct amount of menu items', () => {
|
||||
const wrapper = createWrapper();
|
||||
const items = wrapper.find(DropdownItem);
|
||||
|
||||
expect(items).toHaveLength(expectedNonDividerItems + expectedDividerItems);
|
||||
expect(items.find('[divider]')).toHaveLength(expectedDividerItems);
|
||||
expect(items).toHaveLength(9);
|
||||
expect(items.find('[divider]')).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe('toggles state when toggling modal windows', () => {
|
||||
|
||||
42
test/short-urls/helpers/VisitStatsLink.test.js
Normal file
42
test/short-urls/helpers/VisitStatsLink.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import each from 'jest-each';
|
||||
import { Link } from 'react-router-dom';
|
||||
import VisitStatsLink from '../../../src/short-urls/helpers/VisitStatsLink';
|
||||
|
||||
describe('<VisitStatsLink />', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => wrapper && wrapper.unmount());
|
||||
|
||||
each([
|
||||
[ undefined, undefined ],
|
||||
[ null, null ],
|
||||
[{}, null ],
|
||||
[{}, undefined ],
|
||||
[ null, {}],
|
||||
[ undefined, {}],
|
||||
]).it('only renders a plan span when either server or short URL are not set', (selectedServer, shortUrl) => {
|
||||
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
|
||||
const link = wrapper.find(Link);
|
||||
|
||||
expect(link).toHaveLength(0);
|
||||
expect(wrapper.html()).toEqual('<span>Something</span>');
|
||||
});
|
||||
|
||||
each([
|
||||
[{ id: '1' }, { shortCode: 'abc123' }, '/server/1/short-code/abc123/visits' ],
|
||||
[
|
||||
{ id: '3' },
|
||||
{ shortCode: 'def456', domain: 'example.com' },
|
||||
'/server/3/short-code/def456/visits?domain=example.com',
|
||||
],
|
||||
]).it('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => {
|
||||
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
|
||||
const link = wrapper.find(Link);
|
||||
const to = link.prop('to');
|
||||
|
||||
expect(link).toHaveLength(1);
|
||||
expect(to).toEqual(expectedLink);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import each from 'jest-each';
|
||||
import reducer, {
|
||||
DELETE_SHORT_URL, DELETE_SHORT_URL_ERROR,
|
||||
DELETE_SHORT_URL_ERROR,
|
||||
DELETE_SHORT_URL_START,
|
||||
RESET_DELETE_SHORT_URL,
|
||||
SHORT_URL_DELETED,
|
||||
resetDeleteShortUrl,
|
||||
shortUrlDeleted,
|
||||
deleteShortUrl,
|
||||
} from '../../../src/short-urls/reducers/shortUrlDeletion';
|
||||
|
||||
@@ -26,8 +26,8 @@ describe('shortUrlDeletionReducer', () => {
|
||||
errorData: {},
|
||||
}));
|
||||
|
||||
it('returns shortCode on DELETE_SHORT_URL', () =>
|
||||
expect(reducer(undefined, { type: DELETE_SHORT_URL, shortCode: 'foo' })).toEqual({
|
||||
it('returns shortCode on SHORT_URL_DELETED', () =>
|
||||
expect(reducer(undefined, { type: SHORT_URL_DELETED, shortCode: 'foo' })).toEqual({
|
||||
shortCode: 'foo',
|
||||
loading: false,
|
||||
error: false,
|
||||
@@ -51,11 +51,6 @@ describe('shortUrlDeletionReducer', () => {
|
||||
expect(resetDeleteShortUrl()).toEqual({ type: RESET_DELETE_SHORT_URL }));
|
||||
});
|
||||
|
||||
describe('shortUrlDeleted', () => {
|
||||
it('returns expected action', () =>
|
||||
expect(shortUrlDeleted('abc123')).toEqual({ type: SHORT_URL_DELETED, shortCode: 'abc123' }));
|
||||
});
|
||||
|
||||
describe('deleteShortUrl', () => {
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn().mockReturnValue({ selectedServer: {} });
|
||||
@@ -65,20 +60,22 @@ describe('shortUrlDeletionReducer', () => {
|
||||
getState.mockClear();
|
||||
});
|
||||
|
||||
it('dispatches proper actions if API client request succeeds', async () => {
|
||||
each(
|
||||
[[ undefined ], [ null ], [ 'example.com' ]]
|
||||
).it('dispatches proper actions if API client request succeeds', async (domain) => {
|
||||
const apiClientMock = {
|
||||
deleteShortUrl: jest.fn(() => ''),
|
||||
};
|
||||
const shortCode = 'abc123';
|
||||
|
||||
await deleteShortUrl(() => apiClientMock)(shortCode)(dispatch, getState);
|
||||
await deleteShortUrl(() => apiClientMock)(shortCode, domain)(dispatch, getState);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: DELETE_SHORT_URL_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: DELETE_SHORT_URL, shortCode });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_DELETED, shortCode, domain });
|
||||
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledTimes(1);
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledWith(shortCode);
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledWith(shortCode, domain);
|
||||
});
|
||||
|
||||
it('dispatches proper actions if API client request fails', async () => {
|
||||
@@ -100,7 +97,7 @@ describe('shortUrlDeletionReducer', () => {
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: DELETE_SHORT_URL_ERROR, errorData: data });
|
||||
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledTimes(1);
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledWith(shortCode);
|
||||
expect(apiClientMock.deleteShortUrl).toHaveBeenCalledWith(shortCode, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import moment from 'moment';
|
||||
import each from 'jest-each';
|
||||
import reducer, {
|
||||
EDIT_SHORT_URL_META_START,
|
||||
EDIT_SHORT_URL_META_ERROR,
|
||||
@@ -56,15 +57,15 @@ describe('shortUrlMetaReducer', () => {
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('dispatches metadata on success', async () => {
|
||||
await editShortUrlMeta(buildShlinkApiClient)(shortCode, meta)(dispatch);
|
||||
each([[ undefined ], [ null ], [ 'example.com' ]]).it('dispatches metadata on success', async (domain) => {
|
||||
await editShortUrlMeta(buildShlinkApiClient)(shortCode, domain, meta)(dispatch);
|
||||
|
||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, meta);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, domain, meta);
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_META_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_META_EDITED, meta, shortCode });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_META_EDITED, meta, shortCode, domain });
|
||||
});
|
||||
|
||||
it('dispatches error on failure', async () => {
|
||||
@@ -73,14 +74,14 @@ describe('shortUrlMetaReducer', () => {
|
||||
updateShortUrlMeta.mockRejectedValue(error);
|
||||
|
||||
try {
|
||||
await editShortUrlMeta(buildShlinkApiClient)(shortCode, meta)(dispatch);
|
||||
await editShortUrlMeta(buildShlinkApiClient)(shortCode, undefined, meta)(dispatch);
|
||||
} catch (e) {
|
||||
expect(e).toBe(error);
|
||||
}
|
||||
|
||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, meta);
|
||||
expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, undefined, meta);
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_META_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_META_ERROR });
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import each from 'jest-each';
|
||||
import reducer, {
|
||||
EDIT_SHORT_URL_TAGS,
|
||||
EDIT_SHORT_URL_TAGS_ERROR,
|
||||
EDIT_SHORT_URL_TAGS_START,
|
||||
RESET_EDIT_SHORT_URL_TAGS,
|
||||
resetShortUrlsTags,
|
||||
SHORT_URL_TAGS_EDITED,
|
||||
editShortUrlTags,
|
||||
shortUrlTagsEdited,
|
||||
} from '../../../src/short-urls/reducers/shortUrlTags';
|
||||
|
||||
describe('shortUrlTagsReducer', () => {
|
||||
@@ -28,8 +27,8 @@ describe('shortUrlTagsReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns provided tags and shortCode on EDIT_SHORT_URL_TAGS', () => {
|
||||
expect(reducer({}, { type: EDIT_SHORT_URL_TAGS, tags, shortCode })).toEqual({
|
||||
it('returns provided tags and shortCode on SHORT_URL_TAGS_EDITED', () => {
|
||||
expect(reducer({}, { type: SHORT_URL_TAGS_EDITED, tags, shortCode })).toEqual({
|
||||
tags,
|
||||
shortCode,
|
||||
saving: false,
|
||||
@@ -51,14 +50,6 @@ describe('shortUrlTagsReducer', () => {
|
||||
it('creates expected action', () => expect(resetShortUrlsTags()).toEqual({ type: RESET_EDIT_SHORT_URL_TAGS }));
|
||||
});
|
||||
|
||||
describe('shortUrlTagsEdited', () => {
|
||||
it('creates expected action', () => expect(shortUrlTagsEdited(shortCode, tags)).toEqual({
|
||||
tags,
|
||||
shortCode,
|
||||
type: SHORT_URL_TAGS_EDITED,
|
||||
}));
|
||||
});
|
||||
|
||||
describe('editShortUrlTags', () => {
|
||||
const updateShortUrlTags = jest.fn();
|
||||
const buildShlinkApiClient = jest.fn().mockResolvedValue({ updateShortUrlTags });
|
||||
@@ -70,19 +61,22 @@ describe('shortUrlTagsReducer', () => {
|
||||
dispatch.mockReset();
|
||||
});
|
||||
|
||||
it('dispatches normalized tags on success', async () => {
|
||||
each([[ undefined ], [ null ], [ 'example.com' ]]).it('dispatches normalized tags on success', async (domain) => {
|
||||
const normalizedTags = [ 'bar', 'foo' ];
|
||||
|
||||
updateShortUrlTags.mockResolvedValue(normalizedTags);
|
||||
|
||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, tags)(dispatch);
|
||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, domain, tags)(dispatch);
|
||||
|
||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, tags);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, domain, tags);
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_TAGS, tags: normalizedTags, shortCode });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ type: SHORT_URL_TAGS_EDITED, tags: normalizedTags, shortCode, domain }
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches error on failure', async () => {
|
||||
@@ -91,14 +85,14 @@ describe('shortUrlTagsReducer', () => {
|
||||
updateShortUrlTags.mockRejectedValue(error);
|
||||
|
||||
try {
|
||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, tags)(dispatch);
|
||||
await editShortUrlTags(buildShlinkApiClient)(shortCode, undefined, tags)(dispatch);
|
||||
} catch (e) {
|
||||
expect(e).toBe(error);
|
||||
}
|
||||
|
||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledTimes(1);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, tags);
|
||||
expect(updateShortUrlTags).toHaveBeenCalledWith(shortCode, undefined, tags);
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_TAGS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_TAGS_ERROR });
|
||||
|
||||
@@ -38,15 +38,17 @@ describe('shortUrlsListReducer', () => {
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode, tags: [] },
|
||||
{ shortCode, tags: [], domain: 'example.com' },
|
||||
{ shortCode: 'foo', tags: [] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(state, { type: SHORT_URL_TAGS_EDITED, shortCode, tags })).toEqual({
|
||||
expect(reducer(state, { type: SHORT_URL_TAGS_EDITED, shortCode, tags, domain: null })).toEqual({
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode, tags },
|
||||
{ shortCode, tags: [], domain: 'example.com' },
|
||||
{ shortCode: 'foo', tags: [] },
|
||||
],
|
||||
},
|
||||
@@ -55,6 +57,7 @@ describe('shortUrlsListReducer', () => {
|
||||
|
||||
it('Updates meta on matching URL on SHORT_URL_META_EDITED', () => {
|
||||
const shortCode = 'abc123';
|
||||
const domain = 'example.com';
|
||||
const meta = {
|
||||
maxVisits: 5,
|
||||
validSince: '2020-05-05',
|
||||
@@ -62,16 +65,18 @@ describe('shortUrlsListReducer', () => {
|
||||
const state = {
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode, meta: { maxVisits: 10 } },
|
||||
{ shortCode, meta: { maxVisits: 10 }, domain },
|
||||
{ shortCode, meta: { maxVisits: 50 } },
|
||||
{ shortCode: 'foo', meta: null },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(reducer(state, { type: SHORT_URL_META_EDITED, shortCode, meta })).toEqual({
|
||||
expect(reducer(state, { type: SHORT_URL_META_EDITED, shortCode, meta, domain })).toEqual({
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode, meta },
|
||||
{ shortCode, meta, domain: 'example.com' },
|
||||
{ shortCode, meta: { maxVisits: 50 } },
|
||||
{ shortCode: 'foo', meta: null },
|
||||
],
|
||||
},
|
||||
@@ -84,6 +89,7 @@ describe('shortUrlsListReducer', () => {
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode },
|
||||
{ shortCode, domain: 'example.com' },
|
||||
{ shortCode: 'foo' },
|
||||
],
|
||||
},
|
||||
@@ -91,7 +97,7 @@ describe('shortUrlsListReducer', () => {
|
||||
|
||||
expect(reducer(state, { type: SHORT_URL_DELETED, shortCode })).toEqual({
|
||||
shortUrls: {
|
||||
data: [{ shortCode: 'foo' }],
|
||||
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import ForVersion from '../../src/utils/ForVersion';
|
||||
|
||||
describe('<ForVersion />', () => {
|
||||
let wrapped;
|
||||
|
||||
const renderComponent = (minVersion, currentServerVersion) => {
|
||||
wrapped = mount(
|
||||
<ForVersion minVersion={minVersion} currentServerVersion={currentServerVersion}>
|
||||
<span>Hello</span>
|
||||
</ForVersion>
|
||||
);
|
||||
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
afterEach(() => wrapped && wrapped.unmount());
|
||||
|
||||
it('does not render children when current version is empty', () => {
|
||||
const wrapped = renderComponent('1', '');
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render children when current version is lower than min version', () => {
|
||||
const wrapped = renderComponent('2.0.0', '1.8.3');
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
||||
it('renders children when current version is equal min version', () => {
|
||||
const wrapped = renderComponent('2.0.0', '2.0.0');
|
||||
|
||||
expect(wrapped.html()).toContain('<span>Hello</span>');
|
||||
});
|
||||
|
||||
it('renders children when current version is higher than min version', () => {
|
||||
const wrapped = renderComponent('2.0.0', '2.1.0');
|
||||
|
||||
expect(wrapped.html()).toContain('<span>Hello</span>');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,14 @@
|
||||
import each from 'jest-each';
|
||||
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||
|
||||
describe('ShlinkApiClient', () => {
|
||||
const createAxiosMock = (data) => () => Promise.resolve(data);
|
||||
const createApiClient = (data) => new ShlinkApiClient(createAxiosMock(data));
|
||||
const shortCodesWithDomainCombinations = [
|
||||
[ 'abc123', null ],
|
||||
[ 'abc123', undefined ],
|
||||
[ 'abc123', 'example.com' ],
|
||||
];
|
||||
|
||||
describe('listShortUrls', () => {
|
||||
it('properly returns short URLs list', async () => {
|
||||
@@ -67,43 +73,45 @@ describe('ShlinkApiClient', () => {
|
||||
});
|
||||
|
||||
describe('getShortUrl', () => {
|
||||
it('properly returns short URL', async () => {
|
||||
each(shortCodesWithDomainCombinations).it('properly returns short URL', async (shortCode, domain) => {
|
||||
const expectedShortUrl = { foo: 'bar' };
|
||||
const axiosSpy = jest.fn(createAxiosMock({
|
||||
data: expectedShortUrl,
|
||||
}));
|
||||
const { getShortUrl } = new ShlinkApiClient(axiosSpy);
|
||||
|
||||
const result = await getShortUrl('abc123');
|
||||
const result = await getShortUrl(shortCode, domain);
|
||||
|
||||
expect(expectedShortUrl).toEqual(result);
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/short-urls/abc123',
|
||||
url: `/short-urls/${shortCode}`,
|
||||
method: 'GET',
|
||||
params: domain ? { domain } : {},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateShortUrlTags', () => {
|
||||
it('properly updates short URL tags', async () => {
|
||||
each(shortCodesWithDomainCombinations).it('properly updates short URL tags', async (shortCode, domain) => {
|
||||
const expectedTags = [ 'foo', 'bar' ];
|
||||
const axiosSpy = jest.fn(createAxiosMock({
|
||||
data: { tags: expectedTags },
|
||||
}));
|
||||
const { updateShortUrlTags } = new ShlinkApiClient(axiosSpy);
|
||||
|
||||
const result = await updateShortUrlTags('abc123', expectedTags);
|
||||
const result = await updateShortUrlTags(shortCode, domain, expectedTags);
|
||||
|
||||
expect(expectedTags).toEqual(result);
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/short-urls/abc123/tags',
|
||||
url: `/short-urls/${shortCode}/tags`,
|
||||
method: 'PUT',
|
||||
params: domain ? { domain } : {},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateShortUrlMeta', () => {
|
||||
it('properly updates short URL meta', async () => {
|
||||
each(shortCodesWithDomainCombinations).it('properly updates short URL meta', async (shortCode, domain) => {
|
||||
const expectedMeta = {
|
||||
maxVisits: 50,
|
||||
validSince: '2025-01-01T10:00:00+01:00',
|
||||
@@ -111,12 +119,13 @@ describe('ShlinkApiClient', () => {
|
||||
const axiosSpy = jest.fn(createAxiosMock());
|
||||
const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy);
|
||||
|
||||
const result = await updateShortUrlMeta('abc123', expectedMeta);
|
||||
const result = await updateShortUrlMeta(shortCode, domain, expectedMeta);
|
||||
|
||||
expect(expectedMeta).toEqual(result);
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/short-urls/abc123',
|
||||
url: `/short-urls/${shortCode}`,
|
||||
method: 'PATCH',
|
||||
params: domain ? { domain } : {},
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -172,15 +181,16 @@ describe('ShlinkApiClient', () => {
|
||||
});
|
||||
|
||||
describe('deleteShortUrl', () => {
|
||||
it('properly deletes provided short URL', async () => {
|
||||
each(shortCodesWithDomainCombinations).it('properly deletes provided short URL', async (shortCode, domain) => {
|
||||
const axiosSpy = jest.fn(createAxiosMock({}));
|
||||
const { deleteShortUrl } = new ShlinkApiClient(axiosSpy);
|
||||
|
||||
await deleteShortUrl('abc123');
|
||||
await deleteShortUrl(shortCode, domain);
|
||||
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/short-urls/abc123',
|
||||
url: `/short-urls/${shortCode}`,
|
||||
method: 'DELETE',
|
||||
params: domain ? { domain } : {},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,15 +17,17 @@ describe('<ShortUrlVisits />', () => {
|
||||
const match = {
|
||||
params: { shortCode: 'abc123' },
|
||||
};
|
||||
const location = { search: '' };
|
||||
|
||||
const createComponent = (shortUrlVisits) => {
|
||||
const ShortUrlVisits = createShortUrlVisits({ processStatsFromVisits });
|
||||
const ShortUrlVisits = createShortUrlVisits({ processStatsFromVisits }, () => '');
|
||||
|
||||
wrapper = shallow(
|
||||
<ShortUrlVisits
|
||||
getShortUrlDetail={identity}
|
||||
getShortUrlVisits={getShortUrlVisitsMock}
|
||||
match={match}
|
||||
location={location}
|
||||
shortUrlVisits={shortUrlVisits}
|
||||
shortUrlDetail={{}}
|
||||
cancelGetShortUrlVisits={identity}
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('<VisitsHeader />', () => {
|
||||
it('shows the amount of visits', () => {
|
||||
const visitsBadge = wrapper.find('.badge');
|
||||
|
||||
expect(visitsBadge.html()).toContain(`Visits: <span>${shortUrlVisits.visits.length}</span>`);
|
||||
expect(visitsBadge.html()).toContain(`Visits: <span><strong>${shortUrlVisits.visits.length}</strong></span>`);
|
||||
});
|
||||
|
||||
it('shows when the URL was created', () => {
|
||||
|
||||
Reference in New Issue
Block a user