From 2d60f830f78d089dc70d7dcedf90102b9713b736 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 15 Jan 2020 20:25:58 +0100 Subject: [PATCH 1/8] Improved icons on short URL menu --- src/short-urls/helpers/ShortUrlsRowMenu.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index b5df90f8..c72ffa32 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -5,6 +5,7 @@ import { faEllipsisV as menuIcon, faQrcode as qrIcon, faMinusCircle as deleteIcon, + faEdit as editIcon, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; @@ -20,6 +21,7 @@ import PreviewModal from './PreviewModal'; import QrCodeModal from './QrCodeModal'; import './ShortUrlsRowMenu.scss'; +// FIXME Replace with typescript: (DeleteShortUrlModal component, EditTagsModal component) const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component { static propTypes = { onCopyToClipboard: PropTypes.func, @@ -32,6 +34,7 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls isQrModalOpen: false, isPreviewModalOpen: false, isTagsModalOpen: false, + isMetaModalOpen: false, isDeleteModalOpen: false, }; toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen })); @@ -45,6 +48,7 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls const toggleQrCode = toggleModal('isQrModalOpen'); const togglePreview = toggleModal('isPreviewModalOpen'); const toggleTags = toggleModal('isTagsModalOpen'); + const toggleMeta = toggleModal('isMetaModalOpen'); const toggleDelete = toggleModal('isDeleteModalOpen'); return ( @@ -54,11 +58,11 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls -  Visit stats + Visit stats -  Edit tags + Edit tags class ShortUrls toggle={toggleTags} /> + + Edit metadata + + -  Delete short URL + Delete short URL @@ -77,14 +85,14 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls {showPreviewBtn && ( -  Preview + Preview )} -  QR code + QR code @@ -92,7 +100,7 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls -  Copy to clipboard + Copy to clipboard From 80a8e0b55c0d92b998ca74728a5220c0ff69b0a7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 17 Jan 2020 21:07:59 +0100 Subject: [PATCH 2/8] Created component to edit short URLs meta --- src/reducers/index.js | 2 + src/short-urls/CreateShortUrl.js | 9 +-- src/short-urls/helpers/EditMetaModal.js | 79 +++++++++++++++++++ src/short-urls/helpers/EditTagsModal.js | 6 +- src/short-urls/helpers/ShortUrlsRowMenu.js | 15 ++-- src/short-urls/reducers/shortUrlMeta.js | 52 ++++++++++++ src/short-urls/services/provideServices.js | 9 ++- src/utils/services/ShlinkApiClient.js | 6 +- test/short-urls/helpers/EditTagsModal.test.js | 1 - .../helpers/ShortUrlsRowMenu.test.js | 15 ++-- 10 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 src/short-urls/helpers/EditMetaModal.js create mode 100644 src/short-urls/reducers/shortUrlMeta.js diff --git a/src/reducers/index.js b/src/reducers/index.js index 1fdba738..47656016 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,6 +6,7 @@ import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListPara 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 shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits'; import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail'; import tagsListReducer from '../tags/reducers/tagsList'; @@ -20,6 +21,7 @@ export default combineReducers({ shortUrlCreationResult: shortUrlCreationReducer, shortUrlDeletion: shortUrlDeletionReducer, shortUrlTags: shortUrlTagsReducer, + shortUrlMeta: shortUrlMetaReducer, shortUrlVisits: shortUrlVisitsReducer, shortUrlDetail: shortUrlDetailReducer, tagsList: tagsListReducer, diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index 57d604af..1a07065e 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -2,7 +2,7 @@ import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@forta import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { assoc, dissoc, isEmpty, isNil, pipe, replace, trim } from 'ramda'; import React from 'react'; -import { Collapse } from 'reactstrap'; +import { Collapse, FormGroup, Input } from 'reactstrap'; import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; import Checkbox from '../utils/Checkbox'; @@ -40,9 +40,8 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort const changeTags = (tags) => this.setState({ tags: tags.map(normalizeTag) }); const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => ( -
- + class CreateShort onChange={(e) => this.setState({ [id]: e.target.value })} {...props} /> -
+ ); const renderDateInput = (id, placeholder, props = {}) => (
diff --git a/src/short-urls/helpers/EditMetaModal.js b/src/short-urls/helpers/EditMetaModal.js new file mode 100644 index 00000000..611bb0db --- /dev/null +++ b/src/short-urls/helpers/EditMetaModal.js @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalBody, ModalFooter, ModalHeader, FormGroup, Input } from 'reactstrap'; +import { ExternalLink } from 'react-external-link'; +import moment from 'moment'; +import { shortUrlType } from '../reducers/shortUrlsList'; +import { shortUrlEditMetaType } from '../reducers/shortUrlMeta'; +import DateInput from '../../utils/DateInput'; + +const propTypes = { + isOpen: PropTypes.bool.isRequired, + toggle: PropTypes.func.isRequired, + shortUrl: shortUrlType.isRequired, + shortUrlMeta: shortUrlEditMetaType, + editShortUrlMeta: PropTypes.func, + shortUrlMetaEdited: PropTypes.func, + resetShortUrlMeta: PropTypes.func, +}; + +const dateOrUndefined = (shortUrl, dateName) => { + const date = shortUrl && shortUrl.meta && shortUrl.meta[dateName]; + + return date && moment(date); +}; + +const EditMetaModal = ( + { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, shortUrlMetaEdited, resetShortUrlMeta } +) => { + const { saving, error } = editShortUrlMeta; + const url = shortUrl && (shortUrl.shortUrl || ''); + const validSince = dateOrUndefined(shortUrl, 'validSince'); + const validUntil = dateOrUndefined(shortUrl, 'validUntil'); + + console.log(shortUrlMeta, shortUrlMetaEdited, resetShortUrlMeta, useEffect, useState); + + return ( + + + Edit metadata for + +
+ + + + + + + + + + + {error && ( +
+ Something went wrong while saving the metadata :( +
+ )} +
+ + + + +
+
+ ); +}; + +EditMetaModal.propTypes = propTypes; + +export default EditMetaModal; diff --git a/src/short-urls/helpers/EditTagsModal.js b/src/short-urls/helpers/EditTagsModal.js index 261b12e3..8ea99935 100644 --- a/src/short-urls/helpers/EditTagsModal.js +++ b/src/short-urls/helpers/EditTagsModal.js @@ -9,7 +9,6 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon static propTypes = { isOpen: PropTypes.bool.isRequired, toggle: PropTypes.func.isRequired, - url: PropTypes.string.isRequired, shortUrl: shortUrlType.isRequired, shortUrlTags: shortUrlTagsType, editShortUrlTags: PropTypes.func, @@ -51,12 +50,13 @@ const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Compon } render() { - const { isOpen, toggle, url, shortUrlTags } = this.props; + const { isOpen, toggle, shortUrl, shortUrlTags } = this.props; + const url = shortUrl && (shortUrl.shortUrl || ''); return ( this.refreshShortUrls()}> - Edit tags for {url} + Edit tags for this.setState({ tags })} /> diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index c72ffa32..e8921d04 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -21,8 +21,11 @@ import PreviewModal from './PreviewModal'; import QrCodeModal from './QrCodeModal'; import './ShortUrlsRowMenu.scss'; -// FIXME Replace with typescript: (DeleteShortUrlModal component, EditTagsModal component) -const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component { +const ShortUrlsRowMenu = ( + DeleteShortUrlModal, + EditTagsModal, + EditMetaModal +) => class ShortUrlsRowMenu extends React.Component { static propTypes = { onCopyToClipboard: PropTypes.func, selectedServer: serverType, @@ -64,16 +67,12 @@ const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrls Edit tags - + Edit metadata + Delete short URL diff --git a/src/short-urls/reducers/shortUrlMeta.js b/src/short-urls/reducers/shortUrlMeta.js new file mode 100644 index 00000000..4dee593d --- /dev/null +++ b/src/short-urls/reducers/shortUrlMeta.js @@ -0,0 +1,52 @@ +import { createAction, handleActions } from 'redux-actions'; +import PropTypes from 'prop-types'; +import { shortUrlMetaType } from './shortUrlsList'; + +/* eslint-disable padding-line-between-statements */ +export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START'; +export const EDIT_SHORT_URL_META_ERROR = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_ERROR'; +export const EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META'; +export const RESET_EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/RESET_EDIT_SHORT_URL_META'; +export const SHORT_URL_META_EDITED = 'shlink/shortUrlMeta/SHORT_URL_META_EDITED'; +/* eslint-enable padding-line-between-statements */ + +export const shortUrlEditMetaType = PropTypes.shape({ + shortCode: PropTypes.string, + meta: shortUrlMetaType.isRequired, + saving: PropTypes.bool.isRequired, + error: PropTypes.bool.isRequired, +}); + +const initialState = { + shortCode: null, + meta: {}, + saving: false, + error: false, +}; + +export default handleActions({ + [EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }), + [EDIT_SHORT_URL_META_ERROR]: (state) => ({ ...state, saving: false, error: true }), + [EDIT_SHORT_URL_META]: (state, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }), + [RESET_EDIT_SHORT_URL_META]: () => initialState, +}, initialState); + +export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => async (dispatch, getState) => { + dispatch({ type: EDIT_SHORT_URL_META_START }); + const { updateShortUrlMeta } = await buildShlinkApiClient(getState); + + try { + await updateShortUrlMeta(shortCode, meta); + dispatch({ shortCode, meta, type: EDIT_SHORT_URL_META }); + } catch (e) { + dispatch({ type: EDIT_SHORT_URL_META_ERROR }); + } +}; + +export const resetShortUrlMeta = createAction(RESET_EDIT_SHORT_URL_META); + +export const shortUrlMetaEdited = (shortCode, meta) => ({ + meta, + shortCode, + type: SHORT_URL_META_EDITED, +}); diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index c0fe583b..5ca54cad 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -14,6 +14,7 @@ import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreatio import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../reducers/shortUrlTags'; import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; +import EditMetaModal from '../helpers/EditMetaModal'; const provideServices = (bottle, connect) => { // Components @@ -33,7 +34,7 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'stateFlagTimeout'); - bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); + bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal', 'EditMetaModal'); bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'stateFlagTimeout'); bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult'); @@ -54,6 +55,12 @@ const provideServices = (bottle, connect) => { [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); + bottle.serviceFactory('EditMetaModal', () => EditMetaModal); + bottle.decorator('EditMetaModal', connect( + [ 'shortUrlMeta' ], + [ 'editShortUrlMeta', 'shortUrlMetaEdited', 'resetShortUrlMeta' ] + )); + // Actions bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); diff --git a/src/utils/services/ShlinkApiClient.js b/src/utils/services/ShlinkApiClient.js index 2faabf55..1d9190f7 100644 --- a/src/utils/services/ShlinkApiClient.js +++ b/src/utils/services/ShlinkApiClient.js @@ -23,7 +23,7 @@ export default class ShlinkApiClient { listShortUrls = pipe( (options = {}) => reject(isNil, options), - (options = {}) => this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls) + (options) => this._performRequest('/short-urls', 'GET', options).then((resp) => resp.data.shortUrls) ); createShortUrl = (options) => { @@ -49,6 +49,10 @@ export default class ShlinkApiClient { this._performRequest(`/short-urls/${shortCode}/tags`, 'PUT', {}, { tags }) .then((resp) => resp.data.tags); + updateShortUrlMeta = (shortCode, meta) => + this._performRequest(`/short-urls/${shortCode}`, 'PATCH', {}, meta) + .then(() => ({ meta })); + listTags = () => this._performRequest('/tags', 'GET') .then((resp) => resp.data.tags.data); diff --git a/test/short-urls/helpers/EditTagsModal.test.js b/test/short-urls/helpers/EditTagsModal.test.js index 99c3106f..1f6b4a0d 100644 --- a/test/short-urls/helpers/EditTagsModal.test.js +++ b/test/short-urls/helpers/EditTagsModal.test.js @@ -17,7 +17,6 @@ describe('', () => { wrapper = shallow( ', () => { let wrapper; const DeleteShortUrlModal = () => ''; const EditTagsModal = () => ''; + const EditMetaModal = () => ''; const onCopyToClipboard = jest.fn(); const selectedServer = { id: 'abc123' }; const shortUrl = { @@ -17,7 +18,7 @@ describe('', () => { shortUrl: 'https://doma.in/abc123', }; const createWrapper = (serverVersion = '1.21.1') => { - const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal); + const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal, EditMetaModal); wrapper = shallow( ', () => { }); each([ - [ '1.20.3', 6, 2 ], - [ '1.21.0', 6, 2 ], - [ '1.21.1', 6, 2 ], - [ '2.0.0', 5, 1 ], - [ '2.0.1', 5, 1 ], - [ '2.1.0', 5, 1 ], + [ '1.20.3', 7, 2 ], + [ '1.21.0', 7, 2 ], + [ '1.21.1', 7, 2 ], + [ '2.0.0', 6, 1 ], + [ '2.0.1', 6, 1 ], + [ '2.1.0', 6, 1 ], ]).it('renders correct amount of menu items depending on the version', (version, expectedNonDividerItems, expectedDividerItems) => { const wrapper = createWrapper(version); const items = wrapper.find(DropdownItem); From d44a4b260e75720e764e0056c6d9b7267ba33eb0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 13:07:33 +0100 Subject: [PATCH 3/8] Finished component to allow metadata to be edited on existing short URLs --- src/short-urls/helpers/EditMetaModal.js | 43 +++++++++++++------ src/short-urls/helpers/ShortUrlVisitsCount.js | 2 +- src/short-urls/reducers/shortUrlMeta.js | 26 +++++------ src/short-urls/reducers/shortUrlsList.js | 29 ++++++------- src/short-urls/services/provideServices.js | 10 ++--- src/utils/utils.js | 2 + 6 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/short-urls/helpers/EditMetaModal.js b/src/short-urls/helpers/EditMetaModal.js index 611bb0db..31251dcb 100644 --- a/src/short-urls/helpers/EditMetaModal.js +++ b/src/short-urls/helpers/EditMetaModal.js @@ -1,11 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Modal, ModalBody, ModalFooter, ModalHeader, FormGroup, Input } from 'reactstrap'; +import { Modal, ModalBody, ModalFooter, ModalHeader, FormGroup, Input, UncontrolledTooltip } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { ExternalLink } from 'react-external-link'; import moment from 'moment'; import { shortUrlType } from '../reducers/shortUrlsList'; import { shortUrlEditMetaType } from '../reducers/shortUrlMeta'; import DateInput from '../../utils/DateInput'; +import { formatIsoDate } from '../../utils/utils'; const propTypes = { isOpen: PropTypes.bool.isRequired, @@ -13,8 +16,6 @@ const propTypes = { shortUrl: shortUrlType.isRequired, shortUrlMeta: shortUrlEditMetaType, editShortUrlMeta: PropTypes.func, - shortUrlMetaEdited: PropTypes.func, - resetShortUrlMeta: PropTypes.func, }; const dateOrUndefined = (shortUrl, dateName) => { @@ -24,21 +25,29 @@ const dateOrUndefined = (shortUrl, dateName) => { }; const EditMetaModal = ( - { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, shortUrlMetaEdited, resetShortUrlMeta } + { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta } ) => { - const { saving, error } = editShortUrlMeta; + const { saving, error } = shortUrlMeta; const url = shortUrl && (shortUrl.shortUrl || ''); - const validSince = dateOrUndefined(shortUrl, 'validSince'); - const validUntil = dateOrUndefined(shortUrl, 'validUntil'); - - console.log(shortUrlMeta, shortUrlMetaEdited, resetShortUrlMeta, useEffect, useState); + const [ validSince, setValidSince ] = useState(dateOrUndefined(shortUrl, 'validSince')); + const [ validUntil, setValidUntil ] = useState(dateOrUndefined(shortUrl, 'validUntil')); + const [ maxVisits, setMaxVisits ] = useState(shortUrl && shortUrl.meta && shortUrl.meta.maxVisits); + const doEdit = () => editShortUrlMeta(shortUrl.shortCode, { + maxVisits: maxVisits && parseInt(maxVisits), + validSince: validSince && formatIsoDate(validSince), + validUntil: validUntil && formatIsoDate(validUntil), + }).then(toggle); return ( - Edit metadata for + Edit metadata for + +

Using these metadata properties, you can limit when and how many times your short URL can be visited.

+

If any of the params is not met, the URL will behave as if it was an invalid short URL.

+
-
+ e.preventDefault() || doEdit()}> @@ -54,10 +64,17 @@ const EditMetaModal = ( selected={validUntil} minDate={validSince} isClearable + onChange={setValidUntil} /> - + setMaxVisits(e.target.value)} + /> {error && (
diff --git a/src/short-urls/helpers/ShortUrlVisitsCount.js b/src/short-urls/helpers/ShortUrlVisitsCount.js index 458ce16e..6a5c9359 100644 --- a/src/short-urls/helpers/ShortUrlVisitsCount.js +++ b/src/short-urls/helpers/ShortUrlVisitsCount.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { UncontrolledTooltip } from 'reactstrap'; -import { shortUrlMetaType } from '../reducers/shortUrlsList'; +import { shortUrlMetaType } from '../reducers/shortUrlMeta'; import './ShortUrlVisitsCount.scss'; const propTypes = { diff --git a/src/short-urls/reducers/shortUrlMeta.js b/src/short-urls/reducers/shortUrlMeta.js index 4dee593d..724b3b6a 100644 --- a/src/short-urls/reducers/shortUrlMeta.js +++ b/src/short-urls/reducers/shortUrlMeta.js @@ -1,15 +1,18 @@ -import { createAction, handleActions } from 'redux-actions'; +import { handleActions } from 'redux-actions'; import PropTypes from 'prop-types'; -import { shortUrlMetaType } from './shortUrlsList'; /* eslint-disable padding-line-between-statements */ export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START'; export const EDIT_SHORT_URL_META_ERROR = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_ERROR'; -export const EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META'; -export const RESET_EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/RESET_EDIT_SHORT_URL_META'; export const SHORT_URL_META_EDITED = 'shlink/shortUrlMeta/SHORT_URL_META_EDITED'; /* eslint-enable padding-line-between-statements */ +export const shortUrlMetaType = PropTypes.shape({ + validSince: PropTypes.string, + validUntil: PropTypes.string, + maxVisits: PropTypes.number, +}); + export const shortUrlEditMetaType = PropTypes.shape({ shortCode: PropTypes.string, meta: shortUrlMetaType.isRequired, @@ -27,8 +30,7 @@ const initialState = { export default handleActions({ [EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }), [EDIT_SHORT_URL_META_ERROR]: (state) => ({ ...state, saving: false, error: true }), - [EDIT_SHORT_URL_META]: (state, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }), - [RESET_EDIT_SHORT_URL_META]: () => initialState, + [SHORT_URL_META_EDITED]: (state, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }), }, initialState); export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => async (dispatch, getState) => { @@ -37,16 +39,10 @@ export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => a try { await updateShortUrlMeta(shortCode, meta); - dispatch({ shortCode, meta, type: EDIT_SHORT_URL_META }); + dispatch({ shortCode, meta, type: SHORT_URL_META_EDITED }); } catch (e) { dispatch({ type: EDIT_SHORT_URL_META_ERROR }); + + throw e; } }; - -export const resetShortUrlMeta = createAction(RESET_EDIT_SHORT_URL_META); - -export const shortUrlMetaEdited = (shortCode, meta) => ({ - meta, - shortCode, - type: SHORT_URL_META_EDITED, -}); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index c77462b9..6eadeca2 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -3,6 +3,7 @@ import { assoc, assocPath, propEq, reject } from 'ramda'; 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'; /* eslint-disable padding-line-between-statements */ export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; @@ -10,12 +11,6 @@ export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS'; /* eslint-enable padding-line-between-statements */ -export const shortUrlMetaType = PropTypes.shape({ - validSince: PropTypes.string, - validUntil: PropTypes.string, - maxVisits: PropTypes.number, -}); - export const shortUrlType = PropTypes.shape({ shortCode: PropTypes.string, shortUrl: PropTypes.string, @@ -31,23 +26,25 @@ const initialState = { error: false, }; +const setPropFromActionOnMatchingShortUrl = (prop) => (state, { shortCode, [prop]: propValue }) => assocPath( + [ 'shortUrls', 'data' ], + state.shortUrls.data.map( + (shortUrl) => shortUrl.shortCode === shortCode ? assoc(prop, propValue, shortUrl) : shortUrl + ), + state +); + export default handleActions({ [LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }), [LIST_SHORT_URLS]: (state, { shortUrls }) => ({ loading: false, error: false, shortUrls }), [LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true, shortUrls: {} }), - [SHORT_URL_TAGS_EDITED]: (state, action) => { // eslint-disable-line object-shorthand - const { data } = state.shortUrls; - - return assocPath([ 'shortUrls', 'data' ], data.map((shortUrl) => - shortUrl.shortCode === action.shortCode - ? assoc('tags', action.tags, shortUrl) - : shortUrl), state); - }, - [SHORT_URL_DELETED]: (state, action) => assocPath( + [SHORT_URL_DELETED]: (state, { shortCode }) => assocPath( [ 'shortUrls', 'data' ], - reject(propEq('shortCode', action.shortCode), state.shortUrls.data), + reject(propEq('shortCode', shortCode), state.shortUrls.data), state, ), + [SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'), + [SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl('meta'), }, 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 5ca54cad..5642f784 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -8,13 +8,14 @@ import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu'; import CreateShortUrl from '../CreateShortUrl'; import DeleteShortUrlModal from '../helpers/DeleteShortUrlModal'; import EditTagsModal from '../helpers/EditTagsModal'; +import EditMetaModal from '../helpers/EditMetaModal'; import CreateShortUrlResult from '../helpers/CreateShortUrlResult'; import { listShortUrls } from '../reducers/shortUrlsList'; import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation'; import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../reducers/shortUrlTags'; +import { editShortUrlMeta } from '../reducers/shortUrlMeta'; import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; -import EditMetaModal from '../helpers/EditMetaModal'; const provideServices = (bottle, connect) => { // Components @@ -56,10 +57,7 @@ const provideServices = (bottle, connect) => { )); bottle.serviceFactory('EditMetaModal', () => EditMetaModal); - bottle.decorator('EditMetaModal', connect( - [ 'shortUrlMeta' ], - [ 'editShortUrlMeta', 'shortUrlMetaEdited', 'resetShortUrlMeta' ] - )); + bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta' ])); // Actions bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); @@ -75,6 +73,8 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient'); bottle.serviceFactory('resetDeleteShortUrl', () => resetDeleteShortUrl); bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted); + + bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient'); }; export default provideServices; diff --git a/src/utils/utils.js b/src/utils/utils.js index 917873f9..97595ddc 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -70,3 +70,5 @@ export const versionIsValidSemVer = (version) => { }; export const formatDate = (format = 'YYYY-MM-DD') => (date) => date && date.format ? date.format(format) : date; + +export const formatIsoDate = (date) => date && date.format ? date.format() : date; From 207a8cef206f7cb1752e4afc1d41f49756b698ac Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 13:20:46 +0100 Subject: [PATCH 4/8] Updated tests from modified code --- src/utils/services/ShlinkApiClient.js | 2 +- .../short-urls/reducers/shortUrlsList.test.js | 26 +++++++++++++++++++ test/utils/services/ShlinkApiClient.test.js | 19 ++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/utils/services/ShlinkApiClient.js b/src/utils/services/ShlinkApiClient.js index 1d9190f7..fe9ce69a 100644 --- a/src/utils/services/ShlinkApiClient.js +++ b/src/utils/services/ShlinkApiClient.js @@ -51,7 +51,7 @@ export default class ShlinkApiClient { updateShortUrlMeta = (shortCode, meta) => this._performRequest(`/short-urls/${shortCode}`, 'PATCH', {}, meta) - .then(() => ({ meta })); + .then(() => meta); listTags = () => this._performRequest('/tags', 'GET') diff --git a/test/short-urls/reducers/shortUrlsList.test.js b/test/short-urls/reducers/shortUrlsList.test.js index 5af7b9cd..a34786f3 100644 --- a/test/short-urls/reducers/shortUrlsList.test.js +++ b/test/short-urls/reducers/shortUrlsList.test.js @@ -6,6 +6,7 @@ import reducer, { } from '../../../src/short-urls/reducers/shortUrlsList'; import { SHORT_URL_TAGS_EDITED } from '../../../src/short-urls/reducers/shortUrlTags'; import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; +import { SHORT_URL_META_EDITED } from '../../../src/short-urls/reducers/shortUrlMeta'; describe('shortUrlsListReducer', () => { describe('reducer', () => { @@ -52,6 +53,31 @@ describe('shortUrlsListReducer', () => { }); }); + it('Updates meta on matching URL on SHORT_URL_META_EDITED', () => { + const shortCode = 'abc123'; + const meta = { + maxVisits: 5, + validSince: '2020-05-05', + }; + const state = { + shortUrls: { + data: [ + { shortCode, meta: { maxVisits: 10 } }, + { shortCode: 'foo', meta: null }, + ], + }, + }; + + expect(reducer(state, { type: SHORT_URL_META_EDITED, shortCode, meta })).toEqual({ + shortUrls: { + data: [ + { shortCode, meta }, + { shortCode: 'foo', meta: null }, + ], + }, + }); + }); + it('Removes matching URL on SHORT_URL_DELETED', () => { const shortCode = 'abc123'; const state = { diff --git a/test/utils/services/ShlinkApiClient.test.js b/test/utils/services/ShlinkApiClient.test.js index c2ef2f2a..09697bd6 100644 --- a/test/utils/services/ShlinkApiClient.test.js +++ b/test/utils/services/ShlinkApiClient.test.js @@ -102,6 +102,25 @@ describe('ShlinkApiClient', () => { }); }); + describe('updateShortUrlMeta', () => { + it('properly updates short URL meta', async () => { + const expectedMeta = { + maxVisits: 50, + validSince: '2025-01-01T10:00:00+01:00', + }; + const axiosSpy = jest.fn(createAxiosMock()); + const { updateShortUrlMeta } = new ShlinkApiClient(axiosSpy); + + const result = await updateShortUrlMeta('abc123', expectedMeta); + + expect(expectedMeta).toEqual(result); + expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({ + url: '/short-urls/abc123', + method: 'PATCH', + })); + }); + }); + describe('listTags', () => { it('properly returns list of tags', async () => { const expectedTags = [ 'foo', 'bar' ]; From caa6f7bcd8af4b64226eb0edb7ab959c79474227 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 20:21:59 +0100 Subject: [PATCH 5/8] Created shortUrlMetaReducer test --- test/short-urls/reducers/shortUrlMeta.test.js | 78 +++++++++++++++++++ test/short-urls/reducers/shortUrlTags.test.js | 3 +- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 test/short-urls/reducers/shortUrlMeta.test.js diff --git a/test/short-urls/reducers/shortUrlMeta.test.js b/test/short-urls/reducers/shortUrlMeta.test.js new file mode 100644 index 00000000..1fa74036 --- /dev/null +++ b/test/short-urls/reducers/shortUrlMeta.test.js @@ -0,0 +1,78 @@ +import moment from 'moment'; +import reducer, { + EDIT_SHORT_URL_META_START, + EDIT_SHORT_URL_META_ERROR, + SHORT_URL_META_EDITED, + editShortUrlMeta, +} from '../../../src/short-urls/reducers/shortUrlMeta'; + +describe('shortUrlMetaReducer', () => { + const meta = { + maxVisits: 50, + startDate: moment('2020-01-01').format(), + }; + const shortCode = 'abc123'; + + describe('reducer', () => { + it('returns loading on EDIT_SHORT_URL_META_START', () => { + expect(reducer({}, { type: EDIT_SHORT_URL_META_START })).toEqual({ + saving: true, + error: false, + }); + }); + + it('returns error on EDIT_SHORT_URL_META_ERROR', () => { + expect(reducer({}, { type: EDIT_SHORT_URL_META_ERROR })).toEqual({ + saving: false, + error: true, + }); + }); + + it('returns provided tags and shortCode on SHORT_URL_META_EDITED', () => { + expect(reducer({}, { type: SHORT_URL_META_EDITED, meta, shortCode })).toEqual({ + meta, + shortCode, + saving: false, + error: false, + }); + }); + }); + + describe('editShortUrlMeta', () => { + const updateShortUrlMeta = jest.fn().mockResolvedValue({}); + const buildShlinkApiClient = jest.fn().mockResolvedValue({ updateShortUrlMeta }); + const dispatch = jest.fn(); + + afterEach(jest.clearAllMocks); + + it('dispatches metadata on success', async () => { + await editShortUrlMeta(buildShlinkApiClient)(shortCode, meta)(dispatch); + + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, meta); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_META_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: SHORT_URL_META_EDITED, meta, shortCode }); + }); + + it('dispatches error on failure', async () => { + const error = new Error(); + + updateShortUrlMeta.mockRejectedValue(error); + + try { + await editShortUrlMeta(buildShlinkApiClient)(shortCode, meta)(dispatch); + } catch (e) { + expect(e).toBe(error); + } + + expect(buildShlinkApiClient).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledTimes(1); + expect(updateShortUrlMeta).toHaveBeenCalledWith(shortCode, meta); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_SHORT_URL_META_START }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_META_ERROR }); + }); + }); +}); diff --git a/test/short-urls/reducers/shortUrlTags.test.js b/test/short-urls/reducers/shortUrlTags.test.js index e5c0ea52..12cbeb39 100644 --- a/test/short-urls/reducers/shortUrlTags.test.js +++ b/test/short-urls/reducers/shortUrlTags.test.js @@ -1,10 +1,11 @@ import reducer, { EDIT_SHORT_URL_TAGS, EDIT_SHORT_URL_TAGS_ERROR, - EDIT_SHORT_URL_TAGS_START, editShortUrlTags, + EDIT_SHORT_URL_TAGS_START, RESET_EDIT_SHORT_URL_TAGS, resetShortUrlsTags, SHORT_URL_TAGS_EDITED, + editShortUrlTags, shortUrlTagsEdited, } from '../../../src/short-urls/reducers/shortUrlTags'; From f52bcc53897087f03f0176f485b01ae04232634c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 20:37:12 +0100 Subject: [PATCH 6/8] Ensured state is reset on edit meta modal after closing it --- src/short-urls/helpers/EditMetaModal.js | 14 +++++++++----- src/short-urls/reducers/shortUrlMeta.js | 6 +++++- src/short-urls/services/provideServices.js | 5 +++-- test/short-urls/helpers/EditTagsModal.test.js | 5 +---- test/short-urls/reducers/shortUrlMeta.test.js | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/short-urls/helpers/EditMetaModal.js b/src/short-urls/helpers/EditMetaModal.js index 31251dcb..f4cec441 100644 --- a/src/short-urls/helpers/EditMetaModal.js +++ b/src/short-urls/helpers/EditMetaModal.js @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { ExternalLink } from 'react-external-link'; import moment from 'moment'; +import { pipe } from 'ramda'; import { shortUrlType } from '../reducers/shortUrlsList'; import { shortUrlEditMetaType } from '../reducers/shortUrlMeta'; import DateInput from '../../utils/DateInput'; @@ -16,6 +17,7 @@ const propTypes = { shortUrl: shortUrlType.isRequired, shortUrlMeta: shortUrlEditMetaType, editShortUrlMeta: PropTypes.func, + resetShortUrlMeta: PropTypes.func, }; const dateOrUndefined = (shortUrl, dateName) => { @@ -25,22 +27,24 @@ const dateOrUndefined = (shortUrl, dateName) => { }; const EditMetaModal = ( - { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta } + { isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta } ) => { const { saving, error } = shortUrlMeta; const url = shortUrl && (shortUrl.shortUrl || ''); const [ validSince, setValidSince ] = useState(dateOrUndefined(shortUrl, 'validSince')); const [ validUntil, setValidUntil ] = useState(dateOrUndefined(shortUrl, 'validUntil')); const [ maxVisits, setMaxVisits ] = useState(shortUrl && shortUrl.meta && shortUrl.meta.maxVisits); + + const close = pipe(resetShortUrlMeta, toggle); const doEdit = () => editShortUrlMeta(shortUrl.shortCode, { maxVisits: maxVisits && parseInt(maxVisits), validSince: validSince && formatIsoDate(validSince), validUntil: validUntil && formatIsoDate(validUntil), - }).then(toggle); + }).then(close); return ( - - + + Edit metadata for

Using these metadata properties, you can limit when and how many times your short URL can be visited.

@@ -83,7 +87,7 @@ const EditMetaModal = ( )} - + diff --git a/src/short-urls/reducers/shortUrlMeta.js b/src/short-urls/reducers/shortUrlMeta.js index 724b3b6a..2c799e13 100644 --- a/src/short-urls/reducers/shortUrlMeta.js +++ b/src/short-urls/reducers/shortUrlMeta.js @@ -1,10 +1,11 @@ -import { handleActions } from 'redux-actions'; +import { createAction, handleActions } from 'redux-actions'; import PropTypes from 'prop-types'; /* eslint-disable padding-line-between-statements */ export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START'; export const EDIT_SHORT_URL_META_ERROR = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_ERROR'; export const SHORT_URL_META_EDITED = 'shlink/shortUrlMeta/SHORT_URL_META_EDITED'; +export const RESET_EDIT_SHORT_URL_META = 'shlink/shortUrlMeta/RESET_EDIT_SHORT_URL_META'; /* eslint-enable padding-line-between-statements */ export const shortUrlMetaType = PropTypes.shape({ @@ -31,6 +32,7 @@ export default handleActions({ [EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }), [EDIT_SHORT_URL_META_ERROR]: (state) => ({ ...state, saving: false, error: true }), [SHORT_URL_META_EDITED]: (state, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }), + [RESET_EDIT_SHORT_URL_META]: () => initialState, }, initialState); export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => async (dispatch, getState) => { @@ -46,3 +48,5 @@ export const editShortUrlMeta = (buildShlinkApiClient) => (shortCode, meta) => a throw e; } }; + +export const resetShortUrlMeta = createAction(RESET_EDIT_SHORT_URL_META); diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index 5642f784..a46974c9 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -14,7 +14,7 @@ import { listShortUrls } from '../reducers/shortUrlsList'; import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation'; import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../reducers/shortUrlTags'; -import { editShortUrlMeta } from '../reducers/shortUrlMeta'; +import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta'; import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; const provideServices = (bottle, connect) => { @@ -57,7 +57,7 @@ const provideServices = (bottle, connect) => { )); bottle.serviceFactory('EditMetaModal', () => EditMetaModal); - bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta' ])); + bottle.decorator('EditMetaModal', connect([ 'shortUrlMeta' ], [ 'editShortUrlMeta', 'resetShortUrlMeta' ])); // Actions bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); @@ -75,6 +75,7 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted); bottle.serviceFactory('editShortUrlMeta', editShortUrlMeta, 'buildShlinkApiClient'); + bottle.serviceFactory('resetShortUrlMeta', () => resetShortUrlMeta); }; export default provideServices; diff --git a/test/short-urls/helpers/EditTagsModal.test.js b/test/short-urls/helpers/EditTagsModal.test.js index 1f6b4a0d..2bb99f88 100644 --- a/test/short-urls/helpers/EditTagsModal.test.js +++ b/test/short-urls/helpers/EditTagsModal.test.js @@ -35,10 +35,7 @@ describe('', () => { afterEach(() => { wrapper && wrapper.unmount(); - editShortUrlTags.mockClear(); - shortUrlTagsEdited.mockReset(); - resetShortUrlsTags.mockReset(); - toggle.mockReset(); + jest.clearAllMocks(); }); it('resets tags when component is mounted', () => { diff --git a/test/short-urls/reducers/shortUrlMeta.test.js b/test/short-urls/reducers/shortUrlMeta.test.js index 1fa74036..d1158efd 100644 --- a/test/short-urls/reducers/shortUrlMeta.test.js +++ b/test/short-urls/reducers/shortUrlMeta.test.js @@ -3,7 +3,9 @@ import reducer, { EDIT_SHORT_URL_META_START, EDIT_SHORT_URL_META_ERROR, SHORT_URL_META_EDITED, + RESET_EDIT_SHORT_URL_META, editShortUrlMeta, + resetShortUrlMeta, } from '../../../src/short-urls/reducers/shortUrlMeta'; describe('shortUrlMetaReducer', () => { @@ -36,6 +38,15 @@ describe('shortUrlMetaReducer', () => { error: false, }); }); + + it('goes back to initial state on RESET_EDIT_SHORT_URL_META', () => { + expect(reducer({}, { type: RESET_EDIT_SHORT_URL_META })).toEqual({ + meta: {}, + shortCode: null, + saving: false, + error: false, + }); + }); }); describe('editShortUrlMeta', () => { @@ -75,4 +86,8 @@ describe('shortUrlMetaReducer', () => { expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_SHORT_URL_META_ERROR }); }); }); + + describe('resetShortUrlMeta', () => { + it('creates expected action', () => expect(resetShortUrlMeta()).toEqual({ type: RESET_EDIT_SHORT_URL_META })); + }); }); From 38cad143a0ebdb5a88752aff632cdbad953449d4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 20:59:01 +0100 Subject: [PATCH 7/8] Created EditMetaModal test --- test/short-urls/helpers/EditMetaModal.test.js | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/short-urls/helpers/EditMetaModal.test.js diff --git a/test/short-urls/helpers/EditMetaModal.test.js b/test/short-urls/helpers/EditMetaModal.test.js new file mode 100644 index 00000000..471157fc --- /dev/null +++ b/test/short-urls/helpers/EditMetaModal.test.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { FormGroup, Modal, ModalHeader } from 'reactstrap'; +import each from 'jest-each'; +import EditMetaModal from '../../../src/short-urls/helpers/EditMetaModal'; + +describe('', () => { + let wrapper; + const editShortUrlMeta = jest.fn(() => Promise.resolve()); + const resetShortUrlMeta = jest.fn(); + const toggle = jest.fn(); + const createWrapper = (shortUrl, shortUrlMeta) => { + wrapper = shallow( + + ); + + return wrapper; + }; + + afterEach(() => { + wrapper && wrapper.unmount(); + jest.clearAllMocks(); + }); + + it('properly renders form with components', () => { + const wrapper = createWrapper({}, { saving: false, error: false, meta: {} }); + const error = wrapper.find('.bg-danger'); + const form = wrapper.find('form'); + const formGroup = form.find(FormGroup); + + expect(form).toHaveLength(1); + expect(formGroup).toHaveLength(3); + expect(error).toHaveLength(0); + }); + + each([ + [ true, 'Saving...' ], + [ false, 'Save' ], + ]).it('renders submit button on expected state', (saving, expectedText) => { + const wrapper = createWrapper({}, { saving, error: false, meta: {} }); + const button = wrapper.find('[type="submit"]'); + + expect(button.prop('disabled')).toEqual(saving); + expect(button.text()).toContain(expectedText); + }); + + it('renders error message on error', () => { + const wrapper = createWrapper({}, { saving: false, error: true, meta: {} }); + const error = wrapper.find('.bg-danger'); + + expect(error).toHaveLength(1); + }); + + it('saves meta when form is submit', () => { + const preventDefault = jest.fn(); + const wrapper = createWrapper({}, { saving: false, error: false, meta: {} }); + const form = wrapper.find('form'); + + form.simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(editShortUrlMeta).toHaveBeenCalled(); + }); + + each([ + [ '.btn-link', 'onClick' ], + [ Modal, 'toggle' ], + [ ModalHeader, 'toggle' ], + ]).it('resets meta when modal is toggled in any way', (componentToFind, propToCall) => { + const wrapper = createWrapper({}, { saving: false, error: false, meta: {} }); + const component = wrapper.find(componentToFind); + + component.prop(propToCall)(); + + expect(resetShortUrlMeta).toHaveBeenCalled(); + }); +}); From 1f588c5b13f3334c63f415842ce81b3725b6b60c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 19 Jan 2020 21:00:31 +0100 Subject: [PATCH 8/8] Updated changelog with v2.3.0 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a576fd69..543aee7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## 2.3.0 - 2020-01-19 #### Added * [#174](https://github.com/shlinkio/shlink-web-client/issues/174) Added complete support for Shlink v2.x together with currently supported Shlink versions. * [#164](https://github.com/shlinkio/shlink-web-client/issues/164) Added max visits control on those URLs which have `maxVisits`. * [#178](https://github.com/shlinkio/shlink-web-client/issues/178) Short URLs list can now be filtered by date range. +* [#46](https://github.com/shlinkio/shlink-web-client/issues/46) Allowed short URL's metadata to be edited (`maxVisits`, `validSince` and `validUntil`). #### Changed