From 7949e224e0ff20fa46e3d772fdf707523f6ee54f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 20:42:58 +0200 Subject: [PATCH 1/6] Created modal to edit the loing URL behind a short URL --- src/reducers/index.js | 2 + src/short-urls/helpers/EditMetaModal.js | 4 +- src/short-urls/helpers/EditShortUrlModal.js | 60 +++++++++++++++++++++ src/short-urls/helpers/ShortUrlsRowMenu.js | 11 ++++ src/short-urls/reducers/shortUrlEdition.js | 46 ++++++++++++++++ src/short-urls/services/provideServices.js | 9 ++++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/short-urls/helpers/EditShortUrlModal.js create mode 100644 src/short-urls/reducers/shortUrlEdition.js 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 :( +
+ )} +
+ + + + +
+
+ ); +}; + +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; From 1219a16261119622b20a4113782b60897c29f9f3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 20:47:33 +0200 Subject: [PATCH 2/6] Ensured short URLs list is updated after editing the long URL of a short URL --- src/short-urls/helpers/EditShortUrlModal.js | 13 +++++-------- src/short-urls/reducers/shortUrlEdition.js | 6 +----- src/short-urls/reducers/shortUrlsList.js | 2 ++ src/short-urls/services/provideServices.js | 5 ++--- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/short-urls/helpers/EditShortUrlModal.js b/src/short-urls/helpers/EditShortUrlModal.js index f1558749..ddb65689 100644 --- a/src/short-urls/helpers/EditShortUrlModal.js +++ b/src/short-urls/helpers/EditShortUrlModal.js @@ -2,7 +2,6 @@ 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'; @@ -13,20 +12,18 @@ const propTypes = { shortUrl: shortUrlType.isRequired, shortUrlEdition: ShortUrlEditionType, editShortUrl: PropTypes.func, - resetShortUrlEdition: PropTypes.func, }; -const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShortUrl, resetShortUrlEdition }) => { +const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShortUrl }) => { 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); + const doEdit = () => editShortUrl(shortUrl.shortCode, shortUrl.domain, longUrl).then(toggle); return ( - - + + Edit long URL for
e.preventDefault() || doEdit()}> @@ -47,7 +44,7 @@ const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShor )} - +
diff --git a/src/short-urls/reducers/shortUrlEdition.js b/src/short-urls/reducers/shortUrlEdition.js index ae82fde4..4c545b6d 100644 --- a/src/short-urls/reducers/shortUrlEdition.js +++ b/src/short-urls/reducers/shortUrlEdition.js @@ -1,11 +1,10 @@ -import { createAction, handleActions } from 'redux-actions'; +import { 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({ @@ -26,7 +25,6 @@ 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) => { @@ -42,5 +40,3 @@ export const editShortUrl = (buildShlinkApiClient) => (shortCode, domain, longUr throw e; } }; - -export const resetShortUrlEdition = createAction(RESET_EDIT_SHORT_URL); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index efd8978b..a141dba2 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; import { SHORT_URL_META_EDITED, shortUrlMetaType } from './shortUrlMeta'; +import { SHORT_URL_EDITED } from './shortUrlEdition'; /* eslint-disable padding-line-between-statements */ export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; @@ -54,6 +55,7 @@ export default handleActions({ ), [SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'), [SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl('meta'), + [SHORT_URL_EDITED]: setPropFromActionOnMatchingShortUrl('longUrl'), }, initialState); export const listShortUrls = (buildShlinkApiClient) => (params = {}) => async (dispatch, getState) => { diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index b6b308f5..0dc79a73 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -17,7 +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'; +import { editShortUrl } from '../reducers/shortUrlEdition'; const provideServices = (bottle, connect) => { // Components @@ -64,7 +64,7 @@ const provideServices = (bottle, connect) => { bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta', 'resetShortUrlMeta' ])); bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal); - bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl', 'resetShortUrlEdition' ])); + bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ])); // Actions bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); @@ -83,7 +83,6 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta); bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient'); - bottle.serviceFactory('resetShortUrlEdition', () => resetShortUrlEdition); }; export default provideServices; From 2235592308106d8dac803d38ae8a0ac8248a10c5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 20:50:31 +0200 Subject: [PATCH 3/6] Fixed ShortUrlsRowMenu test --- test/short-urls/helpers/ShortUrlsRowMenu.test.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.js index 8ffd0d38..853b2d9c 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.js +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.js @@ -10,6 +10,7 @@ describe('', () => { const DeleteShortUrlModal = () => ''; const EditTagsModal = () => ''; const EditMetaModal = () => ''; + const EditShortUrlModal = () => ''; const onCopyToClipboard = jest.fn(); const selectedServer = { id: 'abc123' }; const shortUrl = { @@ -17,7 +18,13 @@ describe('', () => { shortUrl: 'https://doma.in/abc123', }; const createWrapper = () => { - const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal, EditMetaModal, () => ''); + const ShortUrlsRowMenu = createShortUrlsRowMenu( + DeleteShortUrlModal, + EditTagsModal, + EditMetaModal, + EditShortUrlModal, + () => '' + ); wrapper = shallow( ', () => { const editTagsModal = wrapper.find(EditTagsModal); const previewModal = wrapper.find(PreviewModal); const qrCodeModal = wrapper.find(QrCodeModal); + const editModal = wrapper.find(EditShortUrlModal); expect(deleteShortUrlModal).toHaveLength(1); expect(editTagsModal).toHaveLength(1); expect(previewModal).toHaveLength(1); expect(qrCodeModal).toHaveLength(1); + expect(editModal).toHaveLength(1); }); it('renders correct amount of menu items', () => { const wrapper = createWrapper(); const items = wrapper.find(DropdownItem); - expect(items).toHaveLength(7); + expect(items).toHaveLength(8); expect(items.find('[divider]')).toHaveLength(1); }); @@ -70,6 +79,7 @@ describe('', () => { it('EditTagsModal', (done) => assert(EditTagsModal, 'isTagsModalOpen', done)); it('PreviewModal', (done) => assert(PreviewModal, 'isPreviewModalOpen', done)); it('QrCodeModal', (done) => assert(QrCodeModal, 'isQrModalOpen', done)); + it('EditShortUrlModal', (done) => assert(EditShortUrlModal, 'isEditModalOpen', done)); }); it('toggles dropdown state when toggling dropdown', (done) => { From b0dd885c09b3f8d0d9491f254dd8ddb92a3392b3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 21:01:01 +0200 Subject: [PATCH 4/6] Converted ShortUrlsRowMenu into functional component --- src/short-urls/helpers/ShortUrlsRowMenu.js | 65 ++++++++----------- .../helpers/ShortUrlsRowMenu.test.js | 34 ++++------ 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index 8dedfbbc..fe490cc8 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -13,47 +13,30 @@ import React from 'react'; import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { serverType } from '../../servers/prop-types'; import { shortUrlType } from '../reducers/shortUrlsList'; +import { useToggle } from '../../utils/helpers/hooks'; import PreviewModal from './PreviewModal'; import QrCodeModal from './QrCodeModal'; import VisitStatsLink from './VisitStatsLink'; import './ShortUrlsRowMenu.scss'; -const ShortUrlsRowMenu = ( - DeleteShortUrlModal, - EditTagsModal, - EditMetaModal, - EditShortUrlModal, - ForServerVersion -) => class ShortUrlsRowMenu extends React.Component { - static propTypes = { - selectedServer: serverType, - shortUrl: shortUrlType, - }; +const propTypes = { + selectedServer: serverType, + shortUrl: shortUrlType, +}; - state = { - isOpen: false, - isQrModalOpen: false, - isPreviewModalOpen: false, - isTagsModalOpen: false, - isMetaModalOpen: false, - isDeleteModalOpen: false, - isEditModalOpen: false, - }; - toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen })); - - render() { - const { shortUrl, selectedServer } = this.props; +const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal, EditMetaModal, EditShortUrlModal, ForServerVersion) => { + const ShortUrlsRowMenuComp = ({ shortUrl, selectedServer }) => { + const [ isOpen, toggle ] = useToggle(false); + const [ isQrModalOpen, toggleQrCode ] = useToggle(false); + const [ isPreviewModalOpen, togglePreview ] = useToggle(false); + const [ isTagsModalOpen, toggleTags ] = useToggle(false); + const [ isMetaModalOpen, toggleMeta ] = useToggle(false); + const [ isDeleteModalOpen, toggleDelete ] = useToggle(false); + const [ isEditModalOpen, toggleEdit ] = useToggle(false); const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : ''; - const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] })); - const toggleQrCode = toggleModal('isQrModalOpen'); - const togglePreview = toggleModal('isPreviewModalOpen'); - const toggleTags = toggleModal('isTagsModalOpen'); - const toggleMeta = toggleModal('isMetaModalOpen'); - const toggleDelete = toggleModal('isDeleteModalOpen'); - const toggleEdit = toggleModal('isEditModalOpen'); return ( - +    @@ -65,32 +48,32 @@ const ShortUrlsRowMenu = ( Edit tags - + Edit metadata - + Edit long URL - + QR code - + Preview - + @@ -98,11 +81,15 @@ const ShortUrlsRowMenu = ( Delete short URL - + ); - } + }; + + ShortUrlsRowMenuComp.propTypes = propTypes; + + return ShortUrlsRowMenuComp; }; export default ShortUrlsRowMenu; diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.js index 853b2d9c..aecd2d84 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.js +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.js @@ -63,34 +63,26 @@ describe('', () => { }); describe('toggles state when toggling modal windows', () => { - const assert = (modalComponent, stateProp, done) => { + const assert = (modalComponent) => { const wrapper = createWrapper(); - const modal = wrapper.find(modalComponent); - expect(wrapper.state(stateProp)).toEqual(false); - modal.prop('toggle')(); - setImmediate(() => { - expect(wrapper.state(stateProp)).toEqual(true); - done(); - }); + expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false); + wrapper.find(modalComponent).prop('toggle')(); + expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(true); }; - it('DeleteShortUrlModal', (done) => assert(DeleteShortUrlModal, 'isDeleteModalOpen', done)); - it('EditTagsModal', (done) => assert(EditTagsModal, 'isTagsModalOpen', done)); - it('PreviewModal', (done) => assert(PreviewModal, 'isPreviewModalOpen', done)); - it('QrCodeModal', (done) => assert(QrCodeModal, 'isQrModalOpen', done)); - it('EditShortUrlModal', (done) => assert(EditShortUrlModal, 'isEditModalOpen', done)); + it('DeleteShortUrlModal', () => assert(DeleteShortUrlModal)); + it('EditTagsModal', () => assert(EditTagsModal)); + it('PreviewModal', () => assert(PreviewModal)); + it('QrCodeModal', () => assert(QrCodeModal)); + it('EditShortUrlModal', () => assert(EditShortUrlModal)); }); - it('toggles dropdown state when toggling dropdown', (done) => { + it('toggles dropdown state when toggling dropdown', () => { const wrapper = createWrapper(); - const dropdown = wrapper.find(ButtonDropdown); - expect(wrapper.state('isOpen')).toEqual(false); - dropdown.prop('toggle')(); - setImmediate(() => { - expect(wrapper.state('isOpen')).toEqual(true); - done(); - }); + expect(wrapper.find(ButtonDropdown).prop('isOpen')).toEqual(false); + wrapper.find(ButtonDropdown).prop('toggle')(); + expect(wrapper.find(ButtonDropdown).prop('isOpen')).toEqual(true); }); }); From a6ad3c2d4d29cc39c950041c9e56ec4de6f38bd9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 21:01:54 +0200 Subject: [PATCH 5/6] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6ebc8a..bc98c657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#213](https://github.com/shlinkio/shlink-web-client/issues/213) The versions of both shlink-web-client and currently consumed Shlink server are now displayed in the footer. * [#221](https://github.com/shlinkio/shlink-web-client/issues/221) Improved how servers are handled, displaying meaningful errors when a not-found or a not-reachable server is tried to be loaded. * [#226](https://github.com/shlinkio/shlink-web-client/issues/226) Created servers can now be edited. -* [#234](https://github.com/shlinkio/shlink-web-client/issues/234) Allowed short code length to be edited on any new short RUL when suing Shlink 2.1 or higher. +* [#234](https://github.com/shlinkio/shlink-web-client/issues/234) Allowed short code length to be edited on any new short URL when suing Shlink 2.1 or higher. +* [#235](https://github.com/shlinkio/shlink-web-client/issues/235) Allowed editing the long URL for any existing short URL when suing Shlink 2.1 or higher. #### Changed From 19337d6c05870ffdd743fe8b99de7e9ba3b0c6d7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 30 Mar 2020 21:26:30 +0200 Subject: [PATCH 6/6] Added tests for elements regarding short URL edition --- .../helpers/EditShortUrlModal.test.js | 79 +++++++++++++++++++ .../reducers/shortUrlEdition.test.js | 74 +++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 test/short-urls/helpers/EditShortUrlModal.test.js create mode 100644 test/short-urls/reducers/shortUrlEdition.test.js diff --git a/test/short-urls/helpers/EditShortUrlModal.test.js b/test/short-urls/helpers/EditShortUrlModal.test.js new file mode 100644 index 00000000..976cde14 --- /dev/null +++ b/test/short-urls/helpers/EditShortUrlModal.test.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { FormGroup, Modal, ModalHeader } from 'reactstrap'; +import EditShortUrlModal from '../../../src/short-urls/helpers/EditShortUrlModal'; + +describe('', () => { + let wrapper; + const editShortUrl = jest.fn(() => Promise.resolve()); + const toggle = jest.fn(); + const createWrapper = (shortUrl, shortUrlEdition) => { + wrapper = shallow( + + ); + + return wrapper; + }; + + afterEach(() => { + wrapper && wrapper.unmount(); + jest.clearAllMocks(); + }); + + it.each([ + [ false, 0 ], + [ true, 1 ], + ])('properly renders form with expected components', (error, expectedErrorLength) => { + const wrapper = createWrapper({}, { saving: false, error }); + const errorElement = wrapper.find('.bg-danger'); + const form = wrapper.find('form'); + const formGroup = form.find(FormGroup); + + expect(form).toHaveLength(1); + expect(formGroup).toHaveLength(1); + expect(errorElement).toHaveLength(expectedErrorLength); + }); + + it.each([ + [ true, 'Saving...', 'something', true ], + [ true, 'Saving...', undefined, true ], + [ false, 'Save', 'something', false ], + [ false, 'Save', undefined, true ], + ])('renders submit button on expected state', (saving, expectedText, longUrl, expectedDisabled) => { + const wrapper = createWrapper({ longUrl }, { saving, error: false }); + const button = wrapper.find('[color="primary"]'); + + expect(button.prop('disabled')).toEqual(expectedDisabled); + expect(button.html()).toContain(expectedText); + }); + + it('saves data when form is submit', () => { + const preventDefault = jest.fn(); + const wrapper = createWrapper({}, { saving: false, error: false }); + const form = wrapper.find('form'); + + form.simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(editShortUrl).toHaveBeenCalled(); + }); + + it.each([ + [ '[color="link"]', 'onClick' ], + [ Modal, 'toggle' ], + [ ModalHeader, 'toggle' ], + ])('toggles modal with different mechanisms', (componentToFind, propToCall) => { + const wrapper = createWrapper({}, { saving: false, error: false }); + const component = wrapper.find(componentToFind); + + component.prop(propToCall)(); + + expect(toggle).toHaveBeenCalled(); + }); +}); diff --git a/test/short-urls/reducers/shortUrlEdition.test.js b/test/short-urls/reducers/shortUrlEdition.test.js new file mode 100644 index 00000000..a64f5492 --- /dev/null +++ b/test/short-urls/reducers/shortUrlEdition.test.js @@ -0,0 +1,74 @@ +import reducer, { + EDIT_SHORT_URL_START, + EDIT_SHORT_URL_ERROR, + SHORT_URL_EDITED, + editShortUrl, +} from '../../../src/short-urls/reducers/shortUrlEdition'; + +describe('shortUrlEditionReducer', () => { + const longUrl = 'https://shlink.io'; + const shortCode = 'abc123'; + + describe('reducer', () => { + it('returns loading on EDIT_SHORT_URL_START', () => { + expect(reducer({}, { type: EDIT_SHORT_URL_START })).toEqual({ + saving: true, + error: false, + }); + }); + + it('returns error on EDIT_SHORT_URL_ERROR', () => { + expect(reducer({}, { type: EDIT_SHORT_URL_ERROR })).toEqual({ + saving: false, + error: true, + }); + }); + + it('returns provided tags and shortCode on SHORT_URL_EDITED', () => { + expect(reducer({}, { type: SHORT_URL_EDITED, longUrl, shortCode })).toEqual({ + longUrl, + shortCode, + saving: false, + error: false, + }); + }); + }); + + describe('editShortUrl', () => { + const updateShortUrlMeta = jest.fn().mockResolvedValue({}); + const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrlMeta }); + const dispatch = jest.fn(); + + afterEach(jest.clearAllMocks); + + it.each([[ undefined ], [ null ], [ 'example.com' ]])('dispatches long URL on success', async (domain) => { + await editShortUrl(buildShlinkApiClient)(shortCode, domain, longUrl)(dispatch); + + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, domain, { longUrl }); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_EDITED, longUrl, shortCode, domain }); + }); + + it('dispatches error on failure', async () => { + const error = new Error(); + + updateShortUrlMeta.mockRejectedValue(error); + + try { + await editShortUrl(buildShlinkApiClient)(shortCode, undefined, longUrl)(dispatch); + } catch (e) { + expect(e).toBe(error); + } + + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, undefined, { longUrl }); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_ERROR }); + }); + }); +});