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
+
+
+
+ );
+};
+
+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);