From f2d03203ae176a2e9c4121567aa8fdcd6b8bfcb4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 16 Sep 2018 10:47:17 +0200 Subject: [PATCH] Implemented short URLs deletion --- src/api/ShlinkApiClient.js | 5 ++ src/reducers/index.js | 6 +- src/short-urls/CreateShortUrl.js | 2 +- .../helpers/CreateShortUrlResult.js | 2 +- src/short-urls/helpers/DeleteShortUrlModal.js | 61 +++++++++++++-- ...lCreationResult.js => shortUrlCreation.js} | 0 src/short-urls/reducers/shortUrlDeletion.js | 76 +++++++++++++++++++ src/short-urls/reducers/shortUrlsList.js | 9 ++- 8 files changed, 150 insertions(+), 11 deletions(-) rename src/short-urls/reducers/{shortUrlCreationResult.js => shortUrlCreation.js} (100%) create mode 100644 src/short-urls/reducers/shortUrlDeletion.js diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index 9355ac81..6c35b84a 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -44,6 +44,11 @@ export class ShlinkApiClient { .then((resp) => resp.data) .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) => this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags }) .then((resp) => resp.data.tags) diff --git a/src/reducers/index.js b/src/reducers/index.js index 54929232..1fdba738 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -3,7 +3,8 @@ import serversReducer from '../servers/reducers/server'; import selectedServerReducer from '../servers/reducers/selectedServer'; import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList'; 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 shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail'; @@ -16,7 +17,8 @@ export default combineReducers({ selectedServer: selectedServerReducer, shortUrlsList: shortUrlsListReducer, shortUrlsListParams: shortUrlsListParamsReducer, - shortUrlCreationResult: shortUrlCreationResultReducer, + shortUrlCreationResult: shortUrlCreationReducer, + shortUrlDeletion: shortUrlDeletionReducer, shortUrlTags: shortUrlTagsReducer, shortUrlVisits: shortUrlVisitsReducer, shortUrlDetail: shortUrlDetailReducer, diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index 5d1bfde6..e3c65819 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -8,7 +8,7 @@ import { Collapse } from 'reactstrap'; import DateInput from '../common/DateInput'; import TagsSelector from '../tags/helpers/TagsSelector'; import CreateShortUrlResult from './helpers/CreateShortUrlResult'; -import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult'; +import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreation'; export class CreateShortUrlComponent extends React.Component { state = { diff --git a/src/short-urls/helpers/CreateShortUrlResult.js b/src/short-urls/helpers/CreateShortUrlResult.js index 5dd2aae4..d8264d03 100644 --- a/src/short-urls/helpers/CreateShortUrlResult.js +++ b/src/short-urls/helpers/CreateShortUrlResult.js @@ -5,7 +5,7 @@ import React from 'react'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Card, CardBody, Tooltip } from 'reactstrap'; import PropTypes from 'prop-types'; -import { createShortUrlResultType } from '../reducers/shortUrlCreationResult'; +import { createShortUrlResultType } from '../reducers/shortUrlCreation'; import { stateFlagTimeout } from '../../utils/utils'; import './CreateShortUrlResult.scss'; diff --git a/src/short-urls/helpers/DeleteShortUrlModal.js b/src/short-urls/helpers/DeleteShortUrlModal.js index 9580329f..7eca1e04 100644 --- a/src/short-urls/helpers/DeleteShortUrlModal.js +++ b/src/short-urls/helpers/DeleteShortUrlModal.js @@ -1,24 +1,56 @@ import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import PropTypes from 'prop-types'; +import { pick, identity } from 'ramda'; import { shortUrlType } from '../reducers/shortUrlsList'; +import { + deleteShortUrl, + resetDeleteShortUrl, + shortUrlDeleted, + shortUrlDeletionType, +} from '../reducers/shortUrlDeletion'; import './QrCodeModal.scss'; -export default class DeleteShortUrlModal extends Component { +export class DeleteShortUrlModalComponent extends Component { static propTypes = { shortUrl: shortUrlType, toggle: PropTypes.func, isOpen: PropTypes.bool, + 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; + + deleteShortUrl(shortCode) + .then(() => { + shortUrlDeleted(shortCode); + toggle(); + }) + .catch(identity); + }; + + componentWillUnmount() { + const { resetDeleteShortUrl } = this.props; + + resetDeleteShortUrl(); + } render() { - const { shortUrl, toggle, isOpen } = this.props; + const { shortUrl, toggle, isOpen, shortUrlDeletion } = this.props; + const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION'; return ( -
e.preventDefault()}> + Delete short URL @@ -33,16 +65,26 @@ export default class DeleteShortUrlModal extends Component { value={this.state.inputValue} onChange={(e) => this.setState({ inputValue: e.target.value })} /> + + {shortUrlDeletion.error && shortUrlDeletion.errorData.error === THRESHOLD_REACHED && ( +
+ This short URL has received too many visits and therefore, it cannot be deleted +
+ )} + {shortUrlDeletion.error && shortUrlDeletion.errorData.error !== THRESHOLD_REACHED && ( +
+ Something went wrong while deleting the URL :( +
+ )}
@@ -50,3 +92,10 @@ export default class DeleteShortUrlModal extends Component { ); } } + +const DeleteShortUrlModal = connect( + pick([ 'shortUrlDeletion' ]), + { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } +)(DeleteShortUrlModalComponent); + +export default DeleteShortUrlModal; diff --git a/src/short-urls/reducers/shortUrlCreationResult.js b/src/short-urls/reducers/shortUrlCreation.js similarity index 100% rename from src/short-urls/reducers/shortUrlCreationResult.js rename to src/short-urls/reducers/shortUrlCreation.js diff --git a/src/short-urls/reducers/shortUrlDeletion.js b/src/short-urls/reducers/shortUrlDeletion.js new file mode 100644 index 00000000..60cdedbe --- /dev/null +++ b/src/short-urls/reducers/shortUrlDeletion.js @@ -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 }); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 1a66bd4e..95e4a7fc 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,7 +1,8 @@ -import { assoc, assocPath } from 'ramda'; +import { assoc, assocPath, reject } from 'ramda'; import PropTypes from 'prop-types'; import shlinkApiClient from '../../api/ShlinkApiClient'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; +import { SHORT_URL_DELETED } from './shortUrlDeletion'; /* eslint-disable padding-line-between-statements, newline-after-var */ 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 ? assoc('tags', action.tags, shortUrl) : shortUrl), state); + case SHORT_URL_DELETED: + return assocPath( + [ 'shortUrls', 'data' ], + reject((shortUrl) => shortUrl.shortCode === action.shortCode, state.shortUrls.data), + state, + ); default: return state; }