From bd2967010895924fb2b290a35fb1caa28b5a3407 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 29 Mar 2020 18:55:41 +0200 Subject: [PATCH 1/3] Added short code length field to form to create short URLs --- src/servers/helpers/ForServerVersion.js | 7 +++---- src/short-urls/CreateShortUrl.js | 28 ++++++++++++++++++------- src/utils/helpers/version.js | 18 ++++++++++------ src/utils/utils.js | 3 ++- test/utils/helpers/version.test.js | 23 ++++++++++++++++++++ 5 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 test/utils/helpers/version.test.js diff --git a/src/servers/helpers/ForServerVersion.js b/src/servers/helpers/ForServerVersion.js index 0c51b22a..0b3c6fba 100644 --- a/src/servers/helpers/ForServerVersion.js +++ b/src/servers/helpers/ForServerVersion.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { serverType } from '../prop-types'; -import { compareVersions } from '../../utils/helpers/version'; +import { versionMatch } from '../../utils/helpers/version'; const propTypes = { minVersion: PropTypes.string, @@ -16,10 +16,9 @@ const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) } const { version } = selectedServer; - const matchesMinVersion = !minVersion || compareVersions(version, '>=', minVersion); - const matchesMaxVersion = !maxVersion || compareVersions(version, '<=', maxVersion); + const matchesVersion = versionMatch(version, { maxVersion, minVersion }); - if (!matchesMinVersion || !matchesMaxVersion) { + if (!matchesVersion) { return null; } diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index e1719fe3..aa1926df 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -7,7 +7,8 @@ import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; import Checkbox from '../utils/Checkbox'; import { serverType } from '../servers/prop-types'; -import { compareVersions } from '../utils/helpers/version'; +import { versionMatch } from '../utils/helpers/version'; +import { hasValue } from '../utils/utils'; import { createShortUrlResultType } from './reducers/shortUrlCreation'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; @@ -30,6 +31,7 @@ const CreateShortUrl = ( longUrl: '', tags: [], customSlug: undefined, + shortCodeLength: undefined, domain: undefined, validSince: undefined, validUntil: undefined, @@ -73,8 +75,9 @@ const CreateShortUrl = ( assoc('validUntil', formatDate(this.state.validUntil)) )(this.state)); }; - const currentServerVersion = this.props.selectedServer ? this.props.selectedServer.version : ''; - const disableDomain = isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', '1.19.0-beta.1'); + const currentServerVersion = this.props.selectedServer && this.props.selectedServer.version; + const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' }); + const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' }); return (
@@ -95,10 +98,19 @@ const CreateShortUrl = (
-
+
{renderOptionalInput('customSlug', 'Custom slug')}
-
+
+ {renderOptionalInput('shortCodeLength', 'Short code length', 'number', { + min: 4, + disabled: disableShortCodeLength || hasValue(this.state.customSlug), + ...disableShortCodeLength && { + title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length', + }, + })} +
+
{renderOptionalInput('domain', 'Domain', 'text', { disabled: disableDomain, ...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' }, @@ -107,13 +119,13 @@ const CreateShortUrl = (
-
+
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
-
+
{renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })}
-
+
{renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })}
diff --git a/src/utils/helpers/version.js b/src/utils/helpers/version.js index 92867939..5d62603a 100644 --- a/src/utils/helpers/version.js +++ b/src/utils/helpers/version.js @@ -1,15 +1,21 @@ import { compare } from 'compare-versions'; import { identity, memoizeWith } from 'ramda'; +import { hasValue } from '../utils'; -export const compareVersions = (firstVersion, operator, secondVersion) => compare( - firstVersion, - secondVersion, - operator, -); +export const versionMatch = (versionToMatch, { maxVersion, minVersion }) => { + if (!hasValue(versionToMatch)) { + return false; + } + + const matchesMinVersion = !minVersion || compare(versionToMatch, minVersion, '>='); + const matchesMaxVersion = !maxVersion || compare(versionToMatch, maxVersion, '<='); + + return !!(matchesMaxVersion && matchesMinVersion); +}; const versionIsValidSemVer = memoizeWith(identity, (version) => { try { - return compareVersions(version, '=', version); + return compare(version, version, '='); } catch (e) { return false; } diff --git a/src/utils/utils.js b/src/utils/utils.js index 0abda883..ee2ee170 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -2,7 +2,7 @@ import L from 'leaflet'; import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; import marker from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; -import { range } from 'ramda'; +import { isEmpty, isNil, range } from 'ramda'; const TEN_ROUNDING_NUMBER = 10; const DEFAULT_TIMEOUT_DELAY = 2000; @@ -45,3 +45,4 @@ export const rangeOf = (size, mappingFn, startAt = 1) => range(startAt, size + 1 export const roundTen = (number) => ceil(number / TEN_ROUNDING_NUMBER) * TEN_ROUNDING_NUMBER; +export const hasValue = (value) => !isNil(value) && !isEmpty(value); diff --git a/test/utils/helpers/version.test.js b/test/utils/helpers/version.test.js new file mode 100644 index 00000000..adcfdee9 --- /dev/null +++ b/test/utils/helpers/version.test.js @@ -0,0 +1,23 @@ +import { versionMatch } from '../../../src/utils/helpers/version'; + +describe('version', () => { + describe('versionMatch', () => { + it.each([ + [ undefined, {}, false ], + [ null, {}, false ], + [ '', {}, false ], + [[], {}, false ], + [ '2.8.3', {}, true ], + [ '2.8.3', { minVersion: '2.0.0' }, true ], + [ '2.0.0', { minVersion: '2.0.0' }, true ], + [ '1.8.0', { maxVersion: '1.8.0' }, true ], + [ '1.7.1', { maxVersion: '1.8.0' }, true ], + [ '1.7.3', { minVersion: '1.7.0', maxVersion: '1.8.0' }, true ], + [ '1.8.3', { minVersion: '2.0.0' }, false ], + [ '1.8.3', { maxVersion: '1.8.0' }, false ], + [ '1.8.3', { minVersion: '1.7.0', maxVersion: '1.8.0' }, false ], + ])('properly matches versions based on what is provided', (version, versionConstraints, expected) => { + expect(versionMatch(version, versionConstraints)).toEqual(expected); + }); + }); +}); From 74ebd4e5721f7f70c6818071ffa74064d0286a28 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 29 Mar 2020 19:36:45 +0200 Subject: [PATCH 2/3] Converted CreateShortUrl to functional component --- package-lock.json | 20 ++--- package.json | 4 +- src/short-urls/CreateShortUrl.js | 109 ++++++++++++------------- test/short-urls/CreateShortUrl.test.js | 54 +++++------- 4 files changed, 85 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 489cb65e..45ce16c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13894,9 +13894,9 @@ } }, "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -14106,14 +14106,14 @@ } }, "react-dom": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz", - "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==", + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", + "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.16.2" + "scheduler": "^0.19.1" } }, "react-error-overlay": { @@ -15279,9 +15279,9 @@ "dev": true }, "scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/package.json b/package.json index ce2b7b2f..9041a0eb 100644 --- a/package.json +++ b/package.json @@ -43,13 +43,13 @@ "prop-types": "^15.7.2", "qs": "^6.9.0", "ramda": "^0.26.1", - "react": "^16.10.2", + "react": "^16.13.1", "react-autosuggest": "^9.4.3", "react-chartjs-2": "^2.8.0", "react-color": "^2.17.3", "react-copy-to-clipboard": "^5.0.1", "react-datepicker": "~1.5.0", - "react-dom": "^16.10.2", + "react-dom": "^16.13.1", "react-external-link": "^1.0.0", "react-leaflet": "^2.4.0", "react-moment": "^0.9.5", diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index aa1926df..32544d9e 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -1,7 +1,7 @@ import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { assoc, dissoc, isEmpty, isNil, pipe, replace, trim } from 'ramda'; -import React from 'react'; +import { isEmpty, isNil, pipe, replace, trim } from 'ramda'; +import React, { useState } from 'react'; import { Collapse, FormGroup, Input } from 'reactstrap'; import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; @@ -9,49 +9,44 @@ import Checkbox from '../utils/Checkbox'; import { serverType } from '../servers/prop-types'; import { versionMatch } from '../utils/helpers/version'; import { hasValue } from '../utils/utils'; +import { useToggle } from '../utils/helpers/hooks'; import { createShortUrlResultType } from './reducers/shortUrlCreation'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; const normalizeTag = pipe(trim, replace(/ /g, '-')); const formatDate = (date) => isNil(date) ? date : date.format(); -const CreateShortUrl = ( - TagsSelector, - CreateShortUrlResult, - ForServerVersion -) => class CreateShortUrl extends React.Component { - static propTypes = { - createShortUrl: PropTypes.func, - shortUrlCreationResult: createShortUrlResultType, - resetCreateShortUrl: PropTypes.func, - selectedServer: serverType, - }; +const propTypes = { + createShortUrl: PropTypes.func, + shortUrlCreationResult: createShortUrlResultType, + resetCreateShortUrl: PropTypes.func, + selectedServer: serverType, +}; - state = { - longUrl: '', - tags: [], - customSlug: undefined, - shortCodeLength: undefined, - domain: undefined, - validSince: undefined, - validUntil: undefined, - maxVisits: undefined, - findIfExists: false, - moreOptionsVisible: false, - }; +const CreateShortUrl = (TagsSelector, CreateShortUrlResult, ForServerVersion) => { + const CreateShortUrlComp = ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }) => { + const [ shortUrlCreation, setShortUrlCreation ] = useState({ + longUrl: '', + tags: [], + customSlug: undefined, + shortCodeLength: undefined, + domain: undefined, + validSince: undefined, + validUntil: undefined, + maxVisits: undefined, + findIfExists: false, + }); + const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle(false); - render() { - const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props; - - const changeTags = (tags) => this.setState({ tags: tags.map(normalizeTag) }); + const changeTags = (tags) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) }); const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => ( this.setState({ [id]: e.target.value })} + value={shortUrlCreation[id]} + onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })} {...props} /> @@ -59,23 +54,23 @@ const CreateShortUrl = ( const renderDateInput = (id, placeholder, props = {}) => (
this.setState({ [id]: date })} + onChange={(date) => setShortUrlCreation({ ...shortUrlCreation, [id]: date })} {...props} />
); const save = (e) => { e.preventDefault(); - createShortUrl(pipe( - dissoc('moreOptionsVisible'), - assoc('validSince', formatDate(this.state.validSince)), - assoc('validUntil', formatDate(this.state.validUntil)) - )(this.state)); + createShortUrl({ + ...shortUrlCreation, + validSince: formatDate(shortUrlCreation.validSince), + validUntil: formatDate(shortUrlCreation.validUntil), + }); }; - const currentServerVersion = this.props.selectedServer && this.props.selectedServer.version; + const currentServerVersion = selectedServer && selectedServer.version; const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' }); const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' }); @@ -87,14 +82,14 @@ const CreateShortUrl = ( type="url" placeholder="Insert the URL to be shortened" required - value={this.state.longUrl} - onChange={(e) => this.setState({ longUrl: e.target.value })} + value={shortUrlCreation.longUrl} + onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })} />
- +
- +
@@ -104,7 +99,7 @@ const CreateShortUrl = (
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', { min: 4, - disabled: disableShortCodeLength || hasValue(this.state.customSlug), + disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug), ...disableShortCodeLength && { title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length', }, @@ -123,10 +118,10 @@ const CreateShortUrl = ( {renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
- {renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })} + {renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil })}
- {renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })} + {renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince })}
@@ -134,8 +129,8 @@ const CreateShortUrl = (
this.setState({ findIfExists })} + checked={shortUrlCreation.findIfExists} + onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })} > Use existing URL if found @@ -145,18 +140,14 @@ const CreateShortUrl = (
- @@ -165,7 +156,11 @@ const CreateShortUrl = ( ); - } + }; + + CreateShortUrlComp.propTypes = propTypes; + + return CreateShortUrlComp; }; export default CreateShortUrl; diff --git a/test/short-urls/CreateShortUrl.test.js b/test/short-urls/CreateShortUrl.test.js index aac1172d..fcb1ec04 100644 --- a/test/short-urls/CreateShortUrl.test.js +++ b/test/short-urls/CreateShortUrl.test.js @@ -25,43 +25,31 @@ describe('', () => { createShortUrl.mockReset(); }); - it('saves short URL with data set in form controls', (done) => { + it('saves short URL with data set in form controls', () => { const validSince = moment('2017-01-01'); const validUntil = moment('2017-01-06'); - const urlInput = wrapper.find('.form-control-lg'); - const tagsInput = wrapper.find(TagsSelector); - const customSlugInput = wrapper.find('#customSlug'); - const domain = wrapper.find('#domain'); - const maxVisitsInput = wrapper.find('#maxVisits'); - const dateInputs = wrapper.find(DateInput); - const validSinceInput = dateInputs.at(0); - const validUntilInput = dateInputs.at(1); + wrapper.find('.form-control-lg').simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } }); + wrapper.find('TagsSelector').simulate('change', [ 'tag_foo', 'tag_bar' ]); + wrapper.find('#customSlug').simulate('change', { target: { value: 'my-slug' } }); + wrapper.find('#domain').simulate('change', { target: { value: 'example.com' } }); + wrapper.find('#maxVisits').simulate('change', { target: { value: '20' } }); + wrapper.find('#shortCodeLength').simulate('change', { target: { value: 15 } }); + wrapper.find(DateInput).at(0).simulate('change', validSince); + wrapper.find(DateInput).at(1).simulate('change', validUntil); + wrapper.find('form').simulate('submit', { preventDefault: identity }); - urlInput.simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } }); - tagsInput.simulate('change', [ 'tag_foo', 'tag_bar' ]); - customSlugInput.simulate('change', { target: { value: 'my-slug' } }); - domain.simulate('change', { target: { value: 'example.com' } }); - maxVisitsInput.simulate('change', { target: { value: '20' } }); - validSinceInput.simulate('change', validSince); - validUntilInput.simulate('change', validUntil); - - setImmediate(() => { - const form = wrapper.find('form'); - - form.simulate('submit', { preventDefault: identity }); - expect(createShortUrl).toHaveBeenCalledTimes(1); - expect(createShortUrl).toHaveBeenCalledWith({ - longUrl: 'https://long-domain.com/foo/bar', - tags: [ 'tag_foo', 'tag_bar' ], - customSlug: 'my-slug', - domain: 'example.com', - validSince: validSince.format(), - validUntil: validUntil.format(), - maxVisits: '20', - findIfExists: false, - }); - done(); + expect(createShortUrl).toHaveBeenCalledTimes(1); + expect(createShortUrl).toHaveBeenCalledWith({ + longUrl: 'https://long-domain.com/foo/bar', + tags: [ 'tag_foo', 'tag_bar' ], + customSlug: 'my-slug', + domain: 'example.com', + validSince: validSince.format(), + validUntil: validUntil.format(), + maxVisits: '20', + findIfExists: false, + shortCodeLength: 15, }); }); }); From a5aab436661a2c6650c70377f50bdfdd5be8f467 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 29 Mar 2020 19:41:29 +0200 Subject: [PATCH 3/3] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c29c2cf7..bf6ebc8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 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. #### Changed