Implemented short URLs deletion

This commit is contained in:
Alejandro Celaya 2018-09-16 10:47:17 +02:00
parent 2d6dda3576
commit f2d03203ae
8 changed files with 150 additions and 11 deletions

View file

@ -44,6 +44,11 @@ export class ShlinkApiClient {
.then((resp) => resp.data) .then((resp) => resp.data)
.catch((e) => this._handleAuthError(e, this.getShortUrl, [ shortCode ])); .catch((e) => this._handleAuthError(e, this.getShortUrl, [ shortCode ]));
deleteShortUrl = (shortCode) =>
this._performRequest(`/short-codes/${shortCode}`, 'DELETE')
.then(() => ({}))
.catch((e) => this._handleAuthError(e, this.deleteShortUrl, [ shortCode ]));
updateShortUrlTags = (shortCode, tags) => updateShortUrlTags = (shortCode, tags) =>
this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags }) this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags })
.then((resp) => resp.data.tags) .then((resp) => resp.data.tags)

View file

@ -3,7 +3,8 @@ import serversReducer from '../servers/reducers/server';
import selectedServerReducer from '../servers/reducers/selectedServer'; import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList'; import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams'; import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
import shortUrlCreationResultReducer from '../short-urls/reducers/shortUrlCreationResult'; import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags'; import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail'; import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail';
@ -16,7 +17,8 @@ export default combineReducers({
selectedServer: selectedServerReducer, selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer, shortUrlsList: shortUrlsListReducer,
shortUrlsListParams: shortUrlsListParamsReducer, shortUrlsListParams: shortUrlsListParamsReducer,
shortUrlCreationResult: shortUrlCreationResultReducer, shortUrlCreationResult: shortUrlCreationReducer,
shortUrlDeletion: shortUrlDeletionReducer,
shortUrlTags: shortUrlTagsReducer, shortUrlTags: shortUrlTagsReducer,
shortUrlVisits: shortUrlVisitsReducer, shortUrlVisits: shortUrlVisitsReducer,
shortUrlDetail: shortUrlDetailReducer, shortUrlDetail: shortUrlDetailReducer,

View file

@ -8,7 +8,7 @@ import { Collapse } from 'reactstrap';
import DateInput from '../common/DateInput'; import DateInput from '../common/DateInput';
import TagsSelector from '../tags/helpers/TagsSelector'; import TagsSelector from '../tags/helpers/TagsSelector';
import CreateShortUrlResult from './helpers/CreateShortUrlResult'; import CreateShortUrlResult from './helpers/CreateShortUrlResult';
import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult'; import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreation';
export class CreateShortUrlComponent extends React.Component { export class CreateShortUrlComponent extends React.Component {
state = { state = {

View file

@ -5,7 +5,7 @@ import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Card, CardBody, Tooltip } from 'reactstrap'; import { Card, CardBody, Tooltip } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createShortUrlResultType } from '../reducers/shortUrlCreationResult'; import { createShortUrlResultType } from '../reducers/shortUrlCreation';
import { stateFlagTimeout } from '../../utils/utils'; import { stateFlagTimeout } from '../../utils/utils';
import './CreateShortUrlResult.scss'; import './CreateShortUrlResult.scss';

View file

@ -1,24 +1,56 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { pick, identity } from 'ramda';
import { shortUrlType } from '../reducers/shortUrlsList'; import { shortUrlType } from '../reducers/shortUrlsList';
import {
deleteShortUrl,
resetDeleteShortUrl,
shortUrlDeleted,
shortUrlDeletionType,
} from '../reducers/shortUrlDeletion';
import './QrCodeModal.scss'; import './QrCodeModal.scss';
export default class DeleteShortUrlModal extends Component { export class DeleteShortUrlModalComponent extends Component {
static propTypes = { static propTypes = {
shortUrl: shortUrlType, shortUrl: shortUrlType,
toggle: PropTypes.func, toggle: PropTypes.func,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
shortUrlDeletion: shortUrlDeletionType,
deleteShortUrl: PropTypes.func,
resetDeleteShortUrl: PropTypes.func,
shortUrlDeleted: PropTypes.func,
}; };
state = { inputValue: '' }; state = { inputValue: '' };
handleDeleteUrl = (e) => {
e.preventDefault();
const { deleteShortUrl, shortUrl, toggle, shortUrlDeleted } = this.props;
const { shortCode } = shortUrl;
deleteShortUrl(shortCode)
.then(() => {
shortUrlDeleted(shortCode);
toggle();
})
.catch(identity);
};
componentWillUnmount() {
const { resetDeleteShortUrl } = this.props;
resetDeleteShortUrl();
}
render() { render() {
const { shortUrl, toggle, isOpen } = this.props; const { shortUrl, toggle, isOpen, shortUrlDeletion } = this.props;
const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION';
return ( return (
<Modal isOpen={isOpen} toggle={toggle} centered> <Modal isOpen={isOpen} toggle={toggle} centered>
<form onSubmit={(e) => e.preventDefault()}> <form onSubmit={this.handleDeleteUrl}>
<ModalHeader toggle={toggle}> <ModalHeader toggle={toggle}>
<span className="text-danger">Delete short URL</span> <span className="text-danger">Delete short URL</span>
</ModalHeader> </ModalHeader>
@ -33,16 +65,26 @@ export default class DeleteShortUrlModal extends Component {
value={this.state.inputValue} value={this.state.inputValue}
onChange={(e) => this.setState({ inputValue: e.target.value })} onChange={(e) => this.setState({ inputValue: e.target.value })}
/> />
{shortUrlDeletion.error && shortUrlDeletion.errorData.error === THRESHOLD_REACHED && (
<div className="p-2 mt-2 bg-warning text-center">
This short URL has received too many visits and therefore, it cannot be deleted
</div>
)}
{shortUrlDeletion.error && shortUrlDeletion.errorData.error !== THRESHOLD_REACHED && (
<div className="p-2 mt-2 bg-danger text-white text-center">
Something went wrong while deleting the URL :(
</div>
)}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button> <button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
<button <button
type="submit" type="submit"
className="btn btn-danger" className="btn btn-danger"
disabled={this.state.inputValue !== shortUrl.shortCode} disabled={this.state.inputValue !== shortUrl.shortCode || shortUrlDeletion.loading}
onClick={toggle}
> >
Delete {shortUrlDeletion.loading ? 'Deleting...' : 'Delete'}
</button> </button>
</ModalFooter> </ModalFooter>
</form> </form>
@ -50,3 +92,10 @@ export default class DeleteShortUrlModal extends Component {
); );
} }
} }
const DeleteShortUrlModal = connect(
pick([ 'shortUrlDeletion' ]),
{ deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted }
)(DeleteShortUrlModalComponent);
export default DeleteShortUrlModal;

View file

@ -0,0 +1,76 @@
import { curry } from 'ramda';
import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient';
/* eslint-disable padding-line-between-statements, newline-after-var */
const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
const DELETE_SHORT_URL_ERROR = 'shlink/deleteShortUrl/DELETE_SHORT_URL_ERROR';
const DELETE_SHORT_URL = 'shlink/deleteShortUrl/DELETE_SHORT_URL';
const RESET_DELETE_SHORT_URL = 'shlink/deleteShortUrl/RESET_DELETE_SHORT_URL';
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
/* eslint-enable padding-line-between-statements, newline-after-var */
export const shortUrlDeletionType = PropTypes.shape({
shortCode: PropTypes.string.isRequired,
loading: PropTypes.bool.isRequired,
error: PropTypes.bool.isRequired,
errorData: PropTypes.shape({
error: PropTypes.string,
message: PropTypes.string,
}).isRequired,
});
const defaultState = {
shortCode: '',
loading: false,
error: false,
errorData: {},
};
export default function reducer(state = defaultState, action) {
switch (action.type) {
case DELETE_SHORT_URL_START:
return {
...state,
loading: true,
error: false,
};
case DELETE_SHORT_URL_ERROR:
return {
...state,
loading: false,
error: true,
errorData: action.errorData,
};
case DELETE_SHORT_URL:
return {
...state,
shortCode: action.shortCode,
loading: false,
error: false,
};
case RESET_DELETE_SHORT_URL:
return defaultState;
default:
return state;
}
}
export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) => {
dispatch({ type: DELETE_SHORT_URL_START });
try {
await shlinkApiClient.deleteShortUrl(shortCode);
dispatch({ type: DELETE_SHORT_URL, shortCode });
} catch (e) {
dispatch({ type: DELETE_SHORT_URL_ERROR, errorData: e.response.data });
throw e;
}
};
export const deleteShortUrl = curry(_deleteShortUrl)(shlinkApiClient);
export const resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL });
export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode });

View file

@ -1,7 +1,8 @@
import { assoc, assocPath } from 'ramda'; import { assoc, assocPath, reject } from 'ramda';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import shlinkApiClient from '../../api/ShlinkApiClient'; import shlinkApiClient from '../../api/ShlinkApiClient';
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
import { SHORT_URL_DELETED } from './shortUrlDeletion';
/* eslint-disable padding-line-between-statements, newline-after-var */ /* eslint-disable padding-line-between-statements, newline-after-var */
const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
@ -43,6 +44,12 @@ export default function reducer(state = initialState, action) {
shortUrl.shortCode === action.shortCode shortUrl.shortCode === action.shortCode
? assoc('tags', action.tags, shortUrl) ? assoc('tags', action.tags, shortUrl)
: shortUrl), state); : shortUrl), state);
case SHORT_URL_DELETED:
return assocPath(
[ 'shortUrls', 'data' ],
reject((shortUrl) => shortUrl.shortCode === action.shortCode, state.shortUrls.data),
state,
);
default: default:
return state; return state;
} }