mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #275 from acelaya-forks/feature/remove-class-components
Feature/remove class components
This commit is contained in:
commit
df38cf6ca9
18 changed files with 347 additions and 506 deletions
|
@ -18,7 +18,8 @@
|
|||
"test:ci": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
|
||||
"test:pretty": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html",
|
||||
"mutate": "./node_modules/.bin/stryker run",
|
||||
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES"
|
||||
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES",
|
||||
"check": "npm run test & npm run lint & wait"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ScrollToTop = ({ scrollTo }) => class ScrollToTop extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
const propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const ScrollToTop = () => {
|
||||
const ScrollToTopComp = ({ location, children }) => {
|
||||
useEffect(() => {
|
||||
scrollTo(0, 0);
|
||||
}, [ location ]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
componentDidUpdate({ location: prevLocation }) {
|
||||
const { location } = this.props;
|
||||
ScrollToTopComp.propTypes = propTypes;
|
||||
|
||||
if (location !== prevLocation) {
|
||||
scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
return ScrollToTopComp;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
|
|
|
@ -1,25 +1,17 @@
|
|||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component {
|
||||
static defaultProps = {
|
||||
onImport: () => ({}),
|
||||
};
|
||||
static propTypes = {
|
||||
onImport: PropTypes.func,
|
||||
createServers: PropTypes.func,
|
||||
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
|
||||
};
|
||||
const propTypes = {
|
||||
onImport: PropTypes.func,
|
||||
createServers: PropTypes.func,
|
||||
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.fileRef = props.fileRef || React.createRef();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { importServersFromFile } = serversImporter;
|
||||
const { onImport, createServers } = this.props;
|
||||
// FIXME Replace with typescript: (ServersImporter)
|
||||
const ImportServersBtn = ({ importServersFromFile }) => {
|
||||
const ImportServersBtnComp = ({ createServers, fileRef, onImport = () => {} }) => {
|
||||
const ref = fileRef || useRef();
|
||||
const onChange = ({ target }) =>
|
||||
importServersFromFile(target.files[0])
|
||||
.then(createServers)
|
||||
|
@ -35,24 +27,22 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea
|
|||
type="button"
|
||||
className="btn btn-outline-secondary mr-2"
|
||||
id="importBtn"
|
||||
onClick={() => this.fileRef.current.click()}
|
||||
onClick={() => ref.current.click()}
|
||||
>
|
||||
Import from file
|
||||
</button>
|
||||
<UncontrolledTooltip placement="top" target="importBtn">
|
||||
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>
|
||||
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>.
|
||||
</UncontrolledTooltip>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="text/csv"
|
||||
className="create-server__csv-select"
|
||||
ref={this.fileRef}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<input type="file" accept="text/csv" className="create-server__csv-select" ref={ref} onChange={onChange} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ImportServersBtnComp.propTypes = propTypes;
|
||||
|
||||
return ImportServersBtnComp;
|
||||
};
|
||||
|
||||
export default ImportServersBtn;
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { isNil } from 'ramda';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Card, CardBody, Tooltip } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createShortUrlResultType } from '../reducers/shortUrlCreation';
|
||||
import './CreateShortUrlResult.scss';
|
||||
|
||||
const CreateShortUrlResult = (stateFlagTimeout) => class CreateShortUrlResult extends React.Component {
|
||||
static propTypes = {
|
||||
resetCreateShortUrl: PropTypes.func,
|
||||
error: PropTypes.bool,
|
||||
result: createShortUrlResultType,
|
||||
};
|
||||
const propTypes = {
|
||||
resetCreateShortUrl: PropTypes.func,
|
||||
error: PropTypes.bool,
|
||||
result: createShortUrlResultType,
|
||||
};
|
||||
|
||||
state = { showCopyTooltip: false };
|
||||
const CreateShortUrlResult = (useStateFlagTimeout) => {
|
||||
const CreateShortUrlResultComp = ({ error, result, resetCreateShortUrl }) => {
|
||||
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
||||
|
||||
componentDidMount() {
|
||||
this.props.resetCreateShortUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, result } = this.props;
|
||||
useEffect(() => {
|
||||
resetCreateShortUrl();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
@ -31,19 +29,19 @@ const CreateShortUrlResult = (stateFlagTimeout) => class CreateShortUrlResult ex
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNil(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { shortUrl } = result;
|
||||
const onCopy = () => stateFlagTimeout(this.setState.bind(this), 'showCopyTooltip');
|
||||
|
||||
return (
|
||||
<Card inverse className="bg-main mt-3">
|
||||
<CardBody>
|
||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
||||
|
||||
<CopyToClipboard text={shortUrl} onCopy={onCopy}>
|
||||
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
|
||||
<button
|
||||
className="btn btn-light btn-sm create-short-url-result__copy-btn"
|
||||
id="copyBtn"
|
||||
|
@ -53,13 +51,17 @@ const CreateShortUrlResult = (stateFlagTimeout) => class CreateShortUrlResult ex
|
|||
</button>
|
||||
</CopyToClipboard>
|
||||
|
||||
<Tooltip placement="left" isOpen={this.state.showCopyTooltip} target="copyBtn">
|
||||
<Tooltip placement="left" isOpen={showCopyTooltip} target="copyBtn">
|
||||
Copied!
|
||||
</Tooltip>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CreateShortUrlResultComp.propTypes = propTypes;
|
||||
|
||||
return CreateShortUrlResultComp;
|
||||
};
|
||||
|
||||
export default CreateShortUrlResult;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { identity, pipe } from 'ramda';
|
||||
|
@ -7,21 +7,28 @@ import { shortUrlDeletionType } from '../reducers/shortUrlDeletion';
|
|||
|
||||
const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION';
|
||||
|
||||
export default class DeleteShortUrlModal extends React.Component {
|
||||
static propTypes = {
|
||||
shortUrl: shortUrlType,
|
||||
toggle: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
shortUrlDeletion: shortUrlDeletionType,
|
||||
deleteShortUrl: PropTypes.func,
|
||||
resetDeleteShortUrl: PropTypes.func,
|
||||
};
|
||||
const propTypes = {
|
||||
shortUrl: shortUrlType,
|
||||
toggle: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
shortUrlDeletion: shortUrlDeletionType,
|
||||
deleteShortUrl: PropTypes.func,
|
||||
resetDeleteShortUrl: PropTypes.func,
|
||||
};
|
||||
|
||||
state = { inputValue: '' };
|
||||
handleDeleteUrl = (e) => {
|
||||
const DeleteShortUrlModal = ({ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }) => {
|
||||
const [ inputValue, setInputValue ] = useState('');
|
||||
|
||||
useEffect(() => resetDeleteShortUrl, []);
|
||||
|
||||
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);
|
||||
const handleDeleteUrl = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { deleteShortUrl, shortUrl, toggle } = this.props;
|
||||
const { shortCode, domain } = shortUrl;
|
||||
|
||||
deleteShortUrl(shortCode, domain)
|
||||
|
@ -29,62 +36,51 @@ export default class DeleteShortUrlModal extends React.Component {
|
|||
.catch(identity);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
const { resetDeleteShortUrl } = this.props;
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={close} centered>
|
||||
<form onSubmit={handleDeleteUrl}>
|
||||
<ModalHeader toggle={close}>
|
||||
<span className="text-danger">Delete short URL</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p><b className="text-danger">Caution!</b> You are about to delete a short URL.</p>
|
||||
<p>This action cannot be undone. Once you have deleted it, all the visits stats will be lost.</p>
|
||||
|
||||
resetDeleteShortUrl();
|
||||
}
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Insert the short code of the URL"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
/>
|
||||
|
||||
render() {
|
||||
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);
|
||||
{hasThresholdError && (
|
||||
<div className="p-2 mt-2 bg-warning text-center">
|
||||
{errorData.threshold && `This short URL has received more than ${errorData.threshold} visits, and therefore, it cannot be deleted.`}
|
||||
{!errorData.threshold && 'This short URL has received too many visits, and therefore, it cannot be deleted.'}
|
||||
</div>
|
||||
)}
|
||||
{hasErrorOtherThanThreshold && (
|
||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||
Something went wrong while deleting the URL :(
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button type="button" className="btn btn-link" onClick={close}>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-danger"
|
||||
disabled={inputValue !== shortUrl.shortCode || shortUrlDeletion.loading}
|
||||
>
|
||||
{shortUrlDeletion.loading ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={close} centered>
|
||||
<form onSubmit={this.handleDeleteUrl}>
|
||||
<ModalHeader toggle={close}>
|
||||
<span className="text-danger">Delete short URL</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p><b className="text-danger">Caution!</b> You are about to delete a short URL.</p>
|
||||
<p>This action cannot be undone. Once you have deleted it, all the visits stats will be lost.</p>
|
||||
DeleteShortUrlModal.propTypes = propTypes;
|
||||
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Insert the short code of the URL"
|
||||
value={this.state.inputValue}
|
||||
onChange={(e) => this.setState({ inputValue: e.target.value })}
|
||||
/>
|
||||
|
||||
{hasThresholdError && (
|
||||
<div className="p-2 mt-2 bg-warning text-center">
|
||||
{errorData.threshold && `This short URL has received more than ${errorData.threshold} visits, and therefore, it cannot be deleted.`}
|
||||
{!errorData.threshold && 'This short URL has received too many visits, and therefore, it cannot be deleted.'}
|
||||
</div>
|
||||
)}
|
||||
{hasErrorOtherThanThreshold && (
|
||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||
Something went wrong while deleting the URL :(
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button type="button" className="btn btn-link" onClick={close}>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-danger"
|
||||
disabled={this.state.inputValue !== shortUrl.shortCode || shortUrlDeletion.loading}
|
||||
>
|
||||
{shortUrlDeletion.loading ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default DeleteShortUrlModal;
|
||||
|
|
|
@ -1,52 +1,37 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } 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';
|
||||
|
||||
const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
shortUrl: shortUrlType.isRequired,
|
||||
shortUrlTags: shortUrlTagsType,
|
||||
editShortUrlTags: PropTypes.func,
|
||||
resetShortUrlsTags: PropTypes.func,
|
||||
};
|
||||
const propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
shortUrl: shortUrlType.isRequired,
|
||||
shortUrlTags: shortUrlTagsType,
|
||||
editShortUrlTags: PropTypes.func,
|
||||
resetShortUrlsTags: PropTypes.func,
|
||||
};
|
||||
|
||||
saveTags = () => {
|
||||
const { editShortUrlTags, shortUrl, toggle } = this.props;
|
||||
const EditTagsModal = (TagsSelector) => {
|
||||
const EditTagsModalComp = ({ isOpen, toggle, shortUrl, shortUrlTags, editShortUrlTags, resetShortUrlsTags }) => {
|
||||
const [ selectedTags, setSelectedTags ] = useState(shortUrl.tags || []);
|
||||
|
||||
editShortUrlTags(shortUrl.shortCode, shortUrl.domain, this.state.tags)
|
||||
useEffect(() => resetShortUrlsTags, []);
|
||||
|
||||
const url = shortUrl && (shortUrl.shortUrl || '');
|
||||
const saveTags = () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags)
|
||||
.then(toggle)
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { resetShortUrlsTags } = this.props;
|
||||
|
||||
resetShortUrlsTags();
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { tags: props.shortUrl.tags };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, toggle, shortUrl, shortUrlTags, resetShortUrlsTags } = this.props;
|
||||
const url = shortUrl && (shortUrl.shortUrl || '');
|
||||
const close = pipe(resetShortUrlsTags, toggle);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={close} centered>
|
||||
<ModalHeader toggle={close}>
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<ModalHeader toggle={toggle}>
|
||||
Edit tags for <ExternalLink href={url} />
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<TagsSelector tags={this.state.tags} onChange={(tags) => this.setState({ tags })} />
|
||||
<TagsSelector tags={selectedTags} onChange={(tags) => setSelectedTags(tags)} />
|
||||
{shortUrlTags.error && (
|
||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||
Something went wrong while saving the tags :(
|
||||
|
@ -54,19 +39,18 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon
|
|||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn btn-link" onClick={close}>Cancel</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
disabled={shortUrlTags.saving}
|
||||
onClick={() => this.saveTags()}
|
||||
>
|
||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||
<button className="btn btn-primary" type="button" disabled={shortUrlTags.saving} onClick={saveTags}>
|
||||
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EditTagsModalComp.propTypes = propTypes;
|
||||
|
||||
return EditTagsModalComp;
|
||||
};
|
||||
|
||||
export default EditTagsModal;
|
||||
|
|
|
@ -46,7 +46,7 @@ const provideServices = (bottle, connect) => {
|
|||
'EditShortUrlModal',
|
||||
'ForServerVersion'
|
||||
);
|
||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'stateFlagTimeout');
|
||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
||||
|
||||
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult', 'ForServerVersion');
|
||||
bottle.decorator(
|
||||
|
|
|
@ -3,64 +3,45 @@ import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
|||
import PropTypes from 'prop-types';
|
||||
import { tagDeleteType } from '../reducers/tagDelete';
|
||||
|
||||
export default class DeleteTagConfirmModal extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
deleteTag: PropTypes.func,
|
||||
tagDelete: tagDeleteType,
|
||||
tagDeleted: PropTypes.func,
|
||||
};
|
||||
|
||||
doDelete = async () => {
|
||||
const { tag, toggle, deleteTag } = this.props;
|
||||
const propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
deleteTag: PropTypes.func,
|
||||
tagDelete: tagDeleteType,
|
||||
tagDeleted: PropTypes.func,
|
||||
};
|
||||
|
||||
const DeleteTagConfirmModal = ({ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }) => {
|
||||
const doDelete = async () => {
|
||||
await deleteTag(tag);
|
||||
this.tagWasDeleted = true;
|
||||
tagDeleted(tag);
|
||||
toggle();
|
||||
};
|
||||
handleOnClosed = () => {
|
||||
if (!this.tagWasDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tagDeleted, tag } = this.props;
|
||||
return (
|
||||
<Modal toggle={toggle} isOpen={isOpen} centered>
|
||||
<ModalHeader toggle={toggle}>
|
||||
<span className="text-danger">Delete tag</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
Are you sure you want to delete tag <b>{tag}</b>?
|
||||
{tagDelete.error && (
|
||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||
Something went wrong while deleting the tag :(
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||
<button className="btn btn-danger" disabled={tagDelete.deleting} onClick={doDelete}>
|
||||
{tagDelete.deleting ? 'Deleting tag...' : 'Delete tag'}
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
tagDeleted(tag);
|
||||
};
|
||||
DeleteTagConfirmModal.propTypes = propTypes;
|
||||
|
||||
componentDidMount() {
|
||||
this.tagWasDeleted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tag, toggle, isOpen, tagDelete } = this.props;
|
||||
|
||||
return (
|
||||
<Modal toggle={toggle} isOpen={isOpen} centered onClosed={this.handleOnClosed}>
|
||||
<ModalHeader toggle={toggle}>
|
||||
<span className="text-danger">Delete tag</span>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
Are you sure you want to delete tag <b>{tag}</b>?
|
||||
{tagDelete.error && (
|
||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||
Something went wrong while deleting the tag :(
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={tagDelete.deleting}
|
||||
onClick={() => this.doDelete()}
|
||||
>
|
||||
{tagDelete.deleting ? 'Deleting tag...' : 'Delete tag'}
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default DeleteTagConfirmModal;
|
||||
|
|
|
@ -1,109 +1,62 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
|
||||
import { ChromePicker } from 'react-color';
|
||||
import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import './EditTagModal.scss';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
|
||||
const EditTagModal = ({ getColorForKey }) => class EditTagModal extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string,
|
||||
editTag: PropTypes.func,
|
||||
toggle: PropTypes.func,
|
||||
tagEdited: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
tagEdit: PropTypes.shape({
|
||||
error: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
const propTypes = {
|
||||
tag: PropTypes.string,
|
||||
editTag: PropTypes.func,
|
||||
toggle: PropTypes.func,
|
||||
tagEdited: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
tagEdit: PropTypes.shape({
|
||||
error: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
saveTag = (e) => {
|
||||
e.preventDefault();
|
||||
const { tag: oldName, editTag, toggle } = this.props;
|
||||
const { tag: newName, color } = this.state;
|
||||
const EditTagModal = ({ getColorForKey }) => {
|
||||
const EditTagModalComp = ({ tag, editTag, toggle, tagEdited, isOpen, tagEdit }) => {
|
||||
const [ newTagName, setNewTagName ] = useState(tag);
|
||||
const [ color, setColor ] = useState(getColorForKey(tag));
|
||||
const [ showColorPicker, toggleColorPicker ] = useToggle();
|
||||
const saveTag = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
editTag(oldName, newName, color)
|
||||
.then(() => {
|
||||
this.tagWasEdited = true;
|
||||
toggle();
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
handleOnClosed = () => {
|
||||
if (!this.tagWasEdited) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tag: oldName, tagEdited } = this.props;
|
||||
const { tag: newName, color } = this.state;
|
||||
|
||||
tagEdited(oldName, newName, color);
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { tag } = props;
|
||||
|
||||
this.state = {
|
||||
showColorPicker: false,
|
||||
tag,
|
||||
color: getColorForKey(tag),
|
||||
editTag(tag, newTagName, color)
|
||||
.then(() => tagEdited(tag, newTagName, color))
|
||||
.then(toggle)
|
||||
.catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.tagWasEdited = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, toggle, tagEdit } = this.props;
|
||||
const { tag, color } = this.state;
|
||||
const toggleColorPicker = () =>
|
||||
this.setState(({ showColorPicker }) => ({ showColorPicker: !showColorPicker }));
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.handleOnClosed}>
|
||||
<form onSubmit={(e) => this.saveTag(e)}>
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<form onSubmit={saveTag}>
|
||||
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="input-group">
|
||||
<div
|
||||
className="input-group-prepend"
|
||||
id="colorPickerBtn"
|
||||
onClick={toggleColorPicker}
|
||||
>
|
||||
<div className="input-group-prepend" id="colorPickerBtn" onClick={toggleColorPicker}>
|
||||
<div
|
||||
className="input-group-text edit-tag-modal__color-picker-toggle"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
}}
|
||||
style={{ backgroundColor: color, borderColor: color }}
|
||||
>
|
||||
<FontAwesomeIcon icon={colorIcon} className="edit-tag-modal__color-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
isOpen={this.state.showColorPicker}
|
||||
toggle={toggleColorPicker}
|
||||
target="colorPickerBtn"
|
||||
placement="right"
|
||||
>
|
||||
<ChromePicker
|
||||
color={color}
|
||||
disableAlpha
|
||||
onChange={(color) => this.setState({ color: color.hex })}
|
||||
/>
|
||||
<Popover isOpen={showColorPicker} toggle={toggleColorPicker} target="colorPickerBtn" placement="right">
|
||||
<ChromePicker color={color} disableAlpha onChange={({ hex }) => setColor(hex)} />
|
||||
</Popover>
|
||||
<input
|
||||
type="text"
|
||||
value={tag}
|
||||
value={newTagName}
|
||||
placeholder="Tag"
|
||||
required
|
||||
className="form-control"
|
||||
onChange={(e) => this.setState({ tag: e.target.value })}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -122,7 +75,11 @@ const EditTagModal = ({ getColorForKey }) => class EditTagModal extends React.Co
|
|||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EditTagModalComp.propTypes = propTypes;
|
||||
|
||||
return EditTagModalComp;
|
||||
};
|
||||
|
||||
export default EditTagModal;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import PropTypes from 'prop-types';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
|
@ -6,28 +6,23 @@ import { identity } from 'ramda';
|
|||
import TagBullet from './TagBullet';
|
||||
import './TagsSelector.scss';
|
||||
|
||||
const TagsSelector = (colorGenerator) => class TagsSelector extends React.Component {
|
||||
static propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
listTags: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
tagsList: PropTypes.shape({
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
};
|
||||
static defaultProps = {
|
||||
placeholder: 'Add tags to the URL',
|
||||
};
|
||||
const propTypes = {
|
||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
listTags: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
tagsList: PropTypes.shape({
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { listTags } = this.props;
|
||||
const TagsSelector = (colorGenerator) => {
|
||||
const TagsSelectorComp = ({ tags, onChange, listTags, tagsList, placeholder = 'Add tags to the URL' }) => {
|
||||
useEffect(() => {
|
||||
listTags();
|
||||
}, []);
|
||||
|
||||
listTags();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tags, onChange, placeholder, tagsList } = this.props;
|
||||
// eslint-disable-next-line
|
||||
const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => (
|
||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||
{getTagDisplayValue(tag)}
|
||||
|
@ -40,7 +35,6 @@ const TagsSelector = (colorGenerator) => class TagsSelector extends React.Compon
|
|||
method === 'enter' ? e.preventDefault() : otherProps.onChange(e);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
const inputValue = (otherProps.value && otherProps.value.trim().toLowerCase()) || '';
|
||||
const inputLength = inputValue.length;
|
||||
const suggestions = tagsList.tags.filter((state) => state.toLowerCase().slice(0, inputLength) === inputValue);
|
||||
|
@ -75,13 +69,16 @@ const TagsSelector = (colorGenerator) => class TagsSelector extends React.Compon
|
|||
onlyUnique
|
||||
renderTag={renderTag}
|
||||
renderInput={renderAutocompleteInput}
|
||||
|
||||
// FIXME Workaround to be able to add tags on Android
|
||||
addOnBlur
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TagsSelectorComp.propTypes = propTypes;
|
||||
|
||||
return TagsSelectorComp;
|
||||
};
|
||||
|
||||
export default TagsSelector;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import axios from 'axios';
|
||||
import { stateFlagTimeout } from '../utils';
|
||||
import { useStateFlagTimeout } from '../helpers/hooks';
|
||||
import Storage from './Storage';
|
||||
import ColorGenerator from './ColorGenerator';
|
||||
|
@ -15,7 +14,6 @@ const provideServices = (bottle) => {
|
|||
|
||||
bottle.constant('setTimeout', global.setTimeout);
|
||||
bottle.constant('clearTimeout', global.clearTimeout);
|
||||
bottle.serviceFactory('stateFlagTimeout', stateFlagTimeout, 'setTimeout');
|
||||
bottle.serviceFactory('useStateFlagTimeout', useStateFlagTimeout, 'setTimeout', 'clearTimeout');
|
||||
};
|
||||
|
||||
|
|
|
@ -4,18 +4,6 @@ import marker from 'leaflet/dist/images/marker-icon.png';
|
|||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
import { isEmpty, isNil, range } from 'ramda';
|
||||
|
||||
const DEFAULT_TIMEOUT_DELAY = 2000;
|
||||
|
||||
export const stateFlagTimeout = (setTimeout) => (
|
||||
setState,
|
||||
flagName,
|
||||
initialValue = true,
|
||||
delay = DEFAULT_TIMEOUT_DELAY
|
||||
) => {
|
||||
setState({ [flagName]: initialValue });
|
||||
setTimeout(() => setState({ [flagName]: !initialValue }), delay);
|
||||
};
|
||||
|
||||
export const determineOrderDir = (clickedField, currentOrderField, currentOrderDir) => {
|
||||
if (currentOrderField !== clickedField) {
|
||||
return 'ASC';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fromPairs, head, keys, pipe, prop, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
|
||||
import SortingDropdown from '../utils/SortingDropdown';
|
||||
|
@ -8,73 +8,78 @@ import { roundTen } from '../utils/helpers/numbers';
|
|||
import SimplePaginator from '../common/SimplePaginator';
|
||||
import GraphCard from './GraphCard';
|
||||
|
||||
const { max } = Math;
|
||||
const propTypes = {
|
||||
stats: PropTypes.object.isRequired,
|
||||
highlightedStats: PropTypes.object,
|
||||
highlightedLabel: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
sortingItems: PropTypes.object.isRequired,
|
||||
extraHeaderContent: PropTypes.func,
|
||||
withPagination: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
const toLowerIfString = (value) => type(value) === 'String' ? toLower(value) : value;
|
||||
const pickKeyFromPair = ([ key ]) => key;
|
||||
const pickValueFromPair = ([ , value ]) => value;
|
||||
|
||||
export default class SortableBarGraph extends React.Component {
|
||||
static propTypes = {
|
||||
stats: PropTypes.object.isRequired,
|
||||
highlightedStats: PropTypes.object,
|
||||
highlightedLabel: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
sortingItems: PropTypes.object.isRequired,
|
||||
extraHeaderContent: PropTypes.func,
|
||||
withPagination: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
const SortableBarGraph = ({
|
||||
stats,
|
||||
highlightedStats,
|
||||
title,
|
||||
sortingItems,
|
||||
extraHeaderContent,
|
||||
withPagination = true,
|
||||
...rest
|
||||
}) => {
|
||||
const [ order, setOrder ] = useState({
|
||||
orderField: undefined,
|
||||
orderDir: undefined,
|
||||
currentPage: 1,
|
||||
itemsPerPage: 50,
|
||||
};
|
||||
});
|
||||
const [ currentPage, setCurrentPage ] = useState(1);
|
||||
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
||||
|
||||
getSortedPairsForStats(stats, sortingItems) {
|
||||
const getSortedPairsForStats = (stats, sortingItems) => {
|
||||
const pairs = toPairs(stats);
|
||||
const sortedPairs = !this.state.orderField ? pairs : sortBy(
|
||||
const sortedPairs = !order.orderField ? pairs : sortBy(
|
||||
pipe(
|
||||
prop(this.state.orderField === head(keys(sortingItems)) ? 0 : 1),
|
||||
prop(order.orderField === head(keys(sortingItems)) ? 0 : 1),
|
||||
toLowerIfString
|
||||
),
|
||||
pairs
|
||||
);
|
||||
|
||||
return !this.state.orderDir || this.state.orderDir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
||||
}
|
||||
|
||||
determineStats(stats, highlightedStats, sortingItems) {
|
||||
const sortedPairs = this.getSortedPairsForStats(stats, sortingItems);
|
||||
return !order.orderDir || order.orderDir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
||||
};
|
||||
const determineStats = (stats, highlightedStats, sortingItems) => {
|
||||
const sortedPairs = getSortedPairsForStats(stats, sortingItems);
|
||||
const sortedKeys = sortedPairs.map(pickKeyFromPair);
|
||||
// The highlighted stats have to be ordered based on the regular stats, not on its own values
|
||||
const sortedHighlightedPairs = highlightedStats && toPairs(
|
||||
{ ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...highlightedStats }
|
||||
);
|
||||
|
||||
if (sortedPairs.length <= this.state.itemsPerPage) {
|
||||
if (sortedPairs.length <= itemsPerPage) {
|
||||
return {
|
||||
currentPageStats: fromPairs(sortedPairs),
|
||||
currentPageHighlightedStats: sortedHighlightedPairs && fromPairs(sortedHighlightedPairs),
|
||||
};
|
||||
}
|
||||
|
||||
const pages = splitEvery(this.state.itemsPerPage, sortedPairs);
|
||||
const highlightedPages = sortedHighlightedPairs && splitEvery(this.state.itemsPerPage, sortedHighlightedPairs);
|
||||
const pages = splitEvery(itemsPerPage, sortedPairs);
|
||||
const highlightedPages = sortedHighlightedPairs && splitEvery(itemsPerPage, sortedHighlightedPairs);
|
||||
|
||||
return {
|
||||
currentPageStats: fromPairs(this.determineCurrentPagePairs(pages)),
|
||||
currentPageHighlightedStats: highlightedPages && fromPairs(this.determineCurrentPagePairs(highlightedPages)),
|
||||
pagination: this.renderPagination(pages.length),
|
||||
max: roundTen(max(...sortedPairs.map(pickValueFromPair))),
|
||||
currentPageStats: fromPairs(determineCurrentPagePairs(pages)),
|
||||
currentPageHighlightedStats: highlightedPages && fromPairs(determineCurrentPagePairs(highlightedPages)),
|
||||
pagination: renderPagination(pages.length),
|
||||
max: roundTen(Math.max(...sortedPairs.map(pickValueFromPair))),
|
||||
};
|
||||
}
|
||||
};
|
||||
const determineCurrentPagePairs = (pages) => {
|
||||
const page = pages[currentPage - 1];
|
||||
|
||||
determineCurrentPagePairs(pages) {
|
||||
const page = pages[this.state.currentPage - 1];
|
||||
|
||||
if (this.state.currentPage < pages.length) {
|
||||
if (currentPage < pages.length) {
|
||||
return page;
|
||||
}
|
||||
|
||||
|
@ -82,72 +87,60 @@ export default class SortableBarGraph extends React.Component {
|
|||
|
||||
// Using the "hidden" key, the chart will just replace the label by an empty string
|
||||
return [ ...page, ...rangeOf(firstPageLength - page.length, (i) => [ `hidden_${i}`, 0 ]) ];
|
||||
}
|
||||
};
|
||||
const renderPagination = (pagesCount) =>
|
||||
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
|
||||
|
||||
renderPagination(pagesCount) {
|
||||
const { currentPage } = this.state;
|
||||
const setCurrentPage = (currentPage) => this.setState({ currentPage });
|
||||
|
||||
return <SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
stats,
|
||||
highlightedStats,
|
||||
sortingItems,
|
||||
title,
|
||||
extraHeaderContent,
|
||||
withPagination = true,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { currentPageStats, currentPageHighlightedStats, pagination, max } = this.determineStats(
|
||||
stats,
|
||||
highlightedStats && keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
||||
sortingItems
|
||||
);
|
||||
const activeCities = keys(currentPageStats);
|
||||
const computeTitle = () => (
|
||||
<React.Fragment>
|
||||
{title}
|
||||
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
|
||||
stats,
|
||||
highlightedStats && keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
||||
sortingItems
|
||||
);
|
||||
const activeCities = keys(currentPageStats);
|
||||
const computeTitle = () => (
|
||||
<React.Fragment>
|
||||
{title}
|
||||
<div className="float-right">
|
||||
<SortingDropdown
|
||||
isButton={false}
|
||||
right
|
||||
items={sortingItems}
|
||||
orderField={order.orderField}
|
||||
orderDir={order.orderDir}
|
||||
onChange={(orderField, orderDir) => setOrder({ orderField, orderDir }) || setCurrentPage(1)}
|
||||
/>
|
||||
</div>
|
||||
{withPagination && keys(stats).length > 50 && (
|
||||
<div className="float-right">
|
||||
<SortingDropdown
|
||||
isButton={false}
|
||||
right
|
||||
items={sortingItems}
|
||||
orderField={this.state.orderField}
|
||||
orderDir={this.state.orderDir}
|
||||
onChange={(orderField, orderDir) => this.setState({ orderField, orderDir, currentPage: 1 })}
|
||||
<PaginationDropdown
|
||||
toggleClassName="btn-sm p-0 mr-3"
|
||||
ranges={[ 50, 100, 200, 500 ]}
|
||||
value={itemsPerPage}
|
||||
setValue={(itemsPerPage) => setItemsPerPage(itemsPerPage) || setCurrentPage(1)}
|
||||
/>
|
||||
</div>
|
||||
{withPagination && keys(stats).length > 50 && (
|
||||
<div className="float-right">
|
||||
<PaginationDropdown
|
||||
toggleClassName="btn-sm p-0 mr-3"
|
||||
ranges={[ 50, 100, 200, 500 ]}
|
||||
value={this.state.itemsPerPage}
|
||||
setValue={(itemsPerPage) => this.setState({ itemsPerPage, currentPage: 1 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{extraHeaderContent && (
|
||||
<div className="float-right">
|
||||
{extraHeaderContent(pagination ? activeCities : undefined)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
)}
|
||||
{extraHeaderContent && (
|
||||
<div className="float-right">
|
||||
{extraHeaderContent(pagination ? activeCities : undefined)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<GraphCard
|
||||
isBarChart
|
||||
title={computeTitle}
|
||||
stats={currentPageStats}
|
||||
highlightedStats={currentPageHighlightedStats}
|
||||
footer={pagination}
|
||||
max={max}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<GraphCard
|
||||
isBarChart
|
||||
title={computeTitle}
|
||||
stats={currentPageStats}
|
||||
highlightedStats={currentPageHighlightedStats}
|
||||
footer={pagination}
|
||||
max={max}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SortableBarGraph.propTypes = propTypes;
|
||||
|
||||
export default SortableBarGraph;
|
||||
|
|
|
@ -20,9 +20,4 @@ describe('<ScrollToTop />', () => {
|
|||
});
|
||||
|
||||
it('just renders children', () => expect(wrapper.text()).toEqual('Foobar'));
|
||||
|
||||
it('scrolls to top when location changes', () => {
|
||||
wrapper.instance().componentDidUpdate({ location: { href: 'bar' } });
|
||||
expect(window.scrollTo).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,17 +7,17 @@ import createCreateShortUrlResult from '../../../src/short-urls/helpers/CreateSh
|
|||
|
||||
describe('<CreateShortUrlResult />', () => {
|
||||
let wrapper;
|
||||
const stateFlagTimeout = jest.fn();
|
||||
const copyToClipboard = jest.fn();
|
||||
const useStateFlagTimeout = jest.fn(() => [ false, copyToClipboard ]);
|
||||
const CreateShortUrlResult = createCreateShortUrlResult(useStateFlagTimeout);
|
||||
const createWrapper = (result, error = false) => {
|
||||
const CreateShortUrlResult = createCreateShortUrlResult(stateFlagTimeout);
|
||||
|
||||
wrapper = shallow(<CreateShortUrlResult resetCreateShortUrl={identity} result={result} error={error} />);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
stateFlagTimeout.mockReset();
|
||||
jest.clearAllMocks();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
|
||||
|
@ -47,8 +47,8 @@ describe('<CreateShortUrlResult />', () => {
|
|||
const wrapper = createWrapper({ shortUrl: 'https://doma.in/abc123' });
|
||||
const copyBtn = wrapper.find(CopyToClipboard);
|
||||
|
||||
expect(stateFlagTimeout).not.toHaveBeenCalled();
|
||||
expect(copyToClipboard).not.toHaveBeenCalled();
|
||||
copyBtn.simulate('copy');
|
||||
expect(stateFlagTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(copyToClipboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,17 +37,6 @@ describe('<EditTagsModal />', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('resets tags when component is mounted', () => {
|
||||
createWrapper({
|
||||
shortCode,
|
||||
tags: [],
|
||||
saving: false,
|
||||
error: false,
|
||||
});
|
||||
|
||||
expect(resetShortUrlsTags).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders tags selector and save button when loaded', () => {
|
||||
const wrapper = createWrapper({
|
||||
shortCode,
|
||||
|
|
|
@ -25,8 +25,7 @@ describe('<DeleteTagConfirmModal />', () => {
|
|||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.unmount();
|
||||
deleteTag.mockReset();
|
||||
tagDeleted.mockReset();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('asks confirmation for provided tag to be deleted', () => {
|
||||
|
@ -56,14 +55,16 @@ describe('<DeleteTagConfirmModal />', () => {
|
|||
expect(delBtn.text()).toEqual('Deleting tag...');
|
||||
});
|
||||
|
||||
it('deletes tag modal when btn is clicked', () => {
|
||||
it('deletes tag modal when btn is clicked', async () => {
|
||||
wrapper = createWrapper({ error: false, deleting: true });
|
||||
const footer = wrapper.find(ModalFooter);
|
||||
const delBtn = footer.find('.btn-danger');
|
||||
|
||||
delBtn.simulate('click');
|
||||
await delBtn.simulate('click');
|
||||
expect(deleteTag).toHaveBeenCalledTimes(1);
|
||||
expect(deleteTag).toHaveBeenCalledWith(tag);
|
||||
expect(tagDeleted).toHaveBeenCalledTimes(1);
|
||||
expect(tagDeleted).toHaveBeenCalledWith(tag);
|
||||
});
|
||||
|
||||
it('does no further actions when modal is closed without deleting tag', () => {
|
||||
|
@ -71,16 +72,7 @@ describe('<DeleteTagConfirmModal />', () => {
|
|||
const modal = wrapper.find(Modal);
|
||||
|
||||
modal.simulate('closed');
|
||||
expect(deleteTag).not.toHaveBeenCalled();
|
||||
expect(tagDeleted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('notifies tag to be deleted when modal is closed after deleting tag', () => {
|
||||
wrapper = createWrapper({ error: false, deleting: false });
|
||||
const modal = wrapper.find(Modal);
|
||||
|
||||
wrapper.instance().tagWasDeleted = true;
|
||||
modal.simulate('closed');
|
||||
expect(tagDeleted).toHaveBeenCalledTimes(1);
|
||||
expect(tagDeleted).toHaveBeenCalledWith(tag);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,31 +2,9 @@ import L from 'leaflet';
|
|||
import marker2x from 'leaflet/dist/images/marker-icon-2x.png';
|
||||
import marker from 'leaflet/dist/images/marker-icon.png';
|
||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
import {
|
||||
stateFlagTimeout as stateFlagTimeoutFactory,
|
||||
determineOrderDir,
|
||||
fixLeafletIcons,
|
||||
rangeOf,
|
||||
} from '../../src/utils/utils';
|
||||
import { determineOrderDir, fixLeafletIcons, rangeOf } from '../../src/utils/utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('stateFlagTimeout', () => {
|
||||
it('sets state and initializes timeout with provided delay', () => {
|
||||
const setTimeout = jest.fn((callback) => callback());
|
||||
const setState = jest.fn();
|
||||
const stateFlagTimeout = stateFlagTimeoutFactory(setTimeout);
|
||||
const delay = 5000;
|
||||
|
||||
stateFlagTimeout(setState, 'foo', false, delay);
|
||||
|
||||
expect(setState).toHaveBeenCalledTimes(2);
|
||||
expect(setState).toHaveBeenNthCalledWith(1, { foo: false });
|
||||
expect(setState).toHaveBeenNthCalledWith(2, { foo: true });
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
expect(setTimeout).toHaveBeenCalledWith(expect.anything(), delay);
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineOrderDir', () => {
|
||||
it('returns ASC when current order field and selected field are different', () => {
|
||||
expect(determineOrderDir('foo', 'bar')).toEqual('ASC');
|
||||
|
|
Loading…
Reference in a new issue