From 80a8e0b55c0d92b998ca74728a5220c0ff69b0a7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 17 Jan 2020 21:07:59 +0100 Subject: [PATCH] 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);