diff --git a/src/reducers/index.js b/src/reducers/index.js index 47656016..2d80d488 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -7,6 +7,7 @@ import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation'; import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion'; import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags'; import shortUrlMetaReducer from '../short-urls/reducers/shortUrlMeta'; +import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition'; import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail'; import tagsListReducer from '../tags/reducers/tagsList'; @@ -22,6 +23,7 @@ export default combineReducers({ shortUrlDeletion: shortUrlDeletionReducer, shortUrlTags: shortUrlTagsReducer, shortUrlMeta: shortUrlMetaReducer, + shortUrlEdition: shortUrlEditionReducer, shortUrlVisits: shortUrlVisitsReducer, shortUrlDetail: shortUrlDetailReducer, tagsList: tagsListReducer, diff --git a/src/short-urls/helpers/EditMetaModal.js b/src/short-urls/helpers/EditMetaModal.js index f7adeb86..aa1cbcb6 100644 --- a/src/short-urls/helpers/EditMetaModal.js +++ b/src/short-urls/helpers/EditMetaModal.js @@ -26,9 +26,7 @@ const dateOrUndefined = (shortUrl, dateName) => { return date && moment(date); }; -const EditMetaModal = ( - { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta } -) => { +const EditMetaModal = ({ isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta }) => { const { saving, error } = shortUrlMeta; const url = shortUrl && (shortUrl.shortUrl || ''); const [ validSince, setValidSince ] = useState(dateOrUndefined(shortUrl, 'validSince')); diff --git a/src/short-urls/helpers/EditShortUrlModal.js b/src/short-urls/helpers/EditShortUrlModal.js new file mode 100644 index 00000000..f1558749 --- /dev/null +++ b/src/short-urls/helpers/EditShortUrlModal.js @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalBody, ModalFooter, ModalHeader, FormGroup, Input, Button } from 'reactstrap'; +import { ExternalLink } from 'react-external-link'; +import { pipe } from 'ramda'; +import { shortUrlType } from '../reducers/shortUrlsList'; +import { ShortUrlEditionType } from '../reducers/shortUrlEdition'; +import { hasValue } from '../../utils/utils'; + +const propTypes = { + isOpen: PropTypes.bool.isRequired, + toggle: PropTypes.func.isRequired, + shortUrl: shortUrlType.isRequired, + shortUrlEdition: ShortUrlEditionType, + editShortUrl: PropTypes.func, + resetShortUrlEdition: PropTypes.func, +}; + +const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShortUrl, resetShortUrlEdition }) => { + const { saving, error } = shortUrlEdition; + const url = shortUrl && (shortUrl.shortUrl || ''); + const [ longUrl, setLongUrl ] = useState(shortUrl.longUrl); + + const close = pipe(resetShortUrlEdition, toggle); + const doEdit = () => editShortUrl(shortUrl.shortCode, shortUrl.domain, longUrl).then(close); + + return ( + + + Edit long URL for + + e.preventDefault() || doEdit()}> + + + setLongUrl(e.target.value)} + /> + + {error && ( + + Something went wrong while saving the long URL :( + + )} + + + Cancel + {saving ? 'Saving...' : 'Save'} + + + + ); +}; + +EditShortUrlModal.propTypes = propTypes; + +export default EditShortUrlModal; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index 0e8a149d..8dedfbbc 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -6,6 +6,7 @@ import { faQrcode as qrIcon, faMinusCircle as deleteIcon, faEdit as editIcon, + faLink as linkIcon, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; @@ -21,6 +22,7 @@ const ShortUrlsRowMenu = ( DeleteShortUrlModal, EditTagsModal, EditMetaModal, + EditShortUrlModal, ForServerVersion ) => class ShortUrlsRowMenu extends React.Component { static propTypes = { @@ -35,6 +37,7 @@ const ShortUrlsRowMenu = ( isTagsModalOpen: false, isMetaModalOpen: false, isDeleteModalOpen: false, + isEditModalOpen: false, }; toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen })); @@ -47,6 +50,7 @@ const ShortUrlsRowMenu = ( const toggleTags = toggleModal('isTagsModalOpen'); const toggleMeta = toggleModal('isMetaModalOpen'); const toggleDelete = toggleModal('isDeleteModalOpen'); + const toggleEdit = toggleModal('isEditModalOpen'); return ( @@ -70,6 +74,13 @@ const ShortUrlsRowMenu = ( + + + Edit long URL + + + + QR code diff --git a/src/short-urls/reducers/shortUrlEdition.js b/src/short-urls/reducers/shortUrlEdition.js new file mode 100644 index 00000000..ae82fde4 --- /dev/null +++ b/src/short-urls/reducers/shortUrlEdition.js @@ -0,0 +1,46 @@ +import { createAction, handleActions } from 'redux-actions'; +import PropTypes from 'prop-types'; + +/* eslint-disable padding-line-between-statements */ +export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START'; +export const EDIT_SHORT_URL_ERROR = 'shlink/shortUrlEdition/EDIT_SHORT_URL_ERROR'; +export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED'; +export const RESET_EDIT_SHORT_URL = 'shlink/shortUrlEdition/RESET_EDIT_SHORT_URL'; +/* eslint-enable padding-line-between-statements */ + +export const ShortUrlEditionType = PropTypes.shape({ + shortCode: PropTypes.string, + longUrl: PropTypes.string, + saving: PropTypes.bool.isRequired, + error: PropTypes.bool.isRequired, +}); + +const initialState = { + shortCode: null, + longUrl: null, + saving: false, + error: false, +}; + +export default handleActions({ + [EDIT_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }), + [EDIT_SHORT_URL_ERROR]: (state) => ({ ...state, saving: false, error: true }), + [SHORT_URL_EDITED]: (state, { shortCode, longUrl }) => ({ shortCode, longUrl, saving: false, error: false }), + [RESET_EDIT_SHORT_URL]: () => initialState, +}, initialState); + +export const editShortUrl = (buildShlinkApiClient) => (shortCode, domain, longUrl) => async (dispatch, getState) => { + dispatch({ type: EDIT_SHORT_URL_START }); + const { updateShortUrlMeta } = buildShlinkApiClient(getState); + + try { + await updateShortUrlMeta(shortCode, domain, { longUrl }); + dispatch({ shortCode, longUrl, domain, type: SHORT_URL_EDITED }); + } catch (e) { + dispatch({ type: EDIT_SHORT_URL_ERROR }); + + throw e; + } +}; + +export const resetShortUrlEdition = createAction(RESET_EDIT_SHORT_URL); diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index 3c3c258f..b6b308f5 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -9,6 +9,7 @@ import CreateShortUrl from '../CreateShortUrl'; import DeleteShortUrlModal from '../helpers/DeleteShortUrlModal'; import EditTagsModal from '../helpers/EditTagsModal'; import EditMetaModal from '../helpers/EditMetaModal'; +import EditShortUrlModal from '../helpers/EditShortUrlModal'; import CreateShortUrlResult from '../helpers/CreateShortUrlResult'; import { listShortUrls } from '../reducers/shortUrlsList'; import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation'; @@ -16,6 +17,7 @@ import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletio import { editShortUrlTags, resetShortUrlsTags } from '../reducers/shortUrlTags'; import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta'; import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; +import { editShortUrl, resetShortUrlEdition } from '../reducers/shortUrlEdition'; const provideServices = (bottle, connect) => { // Components @@ -41,6 +43,7 @@ const provideServices = (bottle, connect) => { 'DeleteShortUrlModal', 'EditTagsModal', 'EditMetaModal', + 'EditShortUrlModal', 'ForServerVersion' ); bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'stateFlagTimeout'); @@ -60,6 +63,9 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('EditMetaModal', () => EditMetaModal); bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta', 'resetShortUrlMeta' ])); + bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal); + bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl', 'resetShortUrlEdition' ])); + // Actions bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); @@ -75,6 +81,9 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient'); bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta); + + bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient'); + bottle.serviceFactory('resetShortUrlEdition', () => resetShortUrlEdition); }; export default provideServices;