diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d467d72..d1f0f887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), #### Changed -* *Nothing* +* [#191](https://github.com/shlinkio/shlink-web-client/issues/191) Created `ForServerVersion` helper component which dynamically renders children if current server conditions are met. #### Deprecated diff --git a/src/container/index.js b/src/container/index.js index 3450796a..483c8acd 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -20,7 +20,7 @@ const mapActionService = (map, actionName) => ({ // Wrap actual action service in a function so that it is lazily created the first time it is called [actionName]: lazyService(container, actionName), }); -const connect = (propsFromState, actionServiceNames) => +const connect = (propsFromState, actionServiceNames = []) => reduxConnect( propsFromState ? pick(propsFromState) : null, actionServiceNames.reduce(mapActionService, {}) diff --git a/src/servers/helpers/ForServerVersion.js b/src/servers/helpers/ForServerVersion.js new file mode 100644 index 00000000..e89eacfc --- /dev/null +++ b/src/servers/helpers/ForServerVersion.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compareVersions } from '../../utils/utils'; +import { serverType } from '../prop-types'; + +const propTypes = { + minVersion: PropTypes.string, + maxVersion: PropTypes.string, + selectedServer: serverType, + children: PropTypes.node.isRequired, +}; + +const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => { + if (!selectedServer) { + return null; + } + + const { version } = selectedServer; + const matchesMinVersion = !minVersion || compareVersions(version, '>=', minVersion); + const matchesMaxVersion = !maxVersion || compareVersions(version, '<=', maxVersion); + + if (!matchesMinVersion || !matchesMaxVersion) { + return null; + } + + return {children}; +}; + +ForServerVersion.propTypes = propTypes; + +export default ForServerVersion; diff --git a/src/servers/prop-types/index.js b/src/servers/prop-types/index.js index 8beba60f..35d43e49 100644 --- a/src/servers/prop-types/index.js +++ b/src/servers/prop-types/index.js @@ -5,4 +5,5 @@ export const serverType = PropTypes.shape({ name: PropTypes.string, url: PropTypes.string, apiKey: PropTypes.string, + version: PropTypes.string, }); diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index 7c87157a..e4231e87 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -6,6 +6,7 @@ import DeleteServerButton from '../DeleteServerButton'; import ImportServersBtn from '../helpers/ImportServersBtn'; import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; import { createServer, createServers, deleteServer, listServers } from '../reducers/server'; +import ForServerVersion from '../helpers/ForServerVersion'; import ServersImporter from './ServersImporter'; import ServersService from './ServersService'; import ServersExporter from './ServersExporter'; @@ -28,6 +29,9 @@ const provideServices = (bottle, connect, withRouter) => { bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ])); + bottle.serviceFactory('ForServerVersion', () => ForServerVersion); + bottle.decorator('ForServerVersion', connect([ 'selectedServer' ])); + // Services bottle.constant('csvjson', csvjson); bottle.service('ServersImporter', ServersImporter, 'csvjson'); diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index 1a07065e..d1ace683 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -6,7 +6,6 @@ import { Collapse, FormGroup, Input } from 'reactstrap'; import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; import Checkbox from '../utils/Checkbox'; -import ForVersion from '../utils/ForVersion'; import { serverType } from '../servers/prop-types'; import { compareVersions } from '../utils/utils'; import { createShortUrlResultType } from './reducers/shortUrlCreation'; @@ -15,7 +14,11 @@ import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; const normalizeTag = pipe(trim, replace(/ /g, '-')); const formatDate = (date) => isNil(date) ? date : date.format(); -const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShortUrl extends React.Component { +const CreateShortUrl = ( + TagsSelector, + CreateShortUrlResult, + ForServerVersion +) => class CreateShortUrl extends React.Component { static propTypes = { createShortUrl: PropTypes.func, shortUrlCreationResult: createShortUrlResultType, @@ -116,7 +119,7 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort - +
class CreateShort
-
+
diff --git a/src/short-urls/SearchBar.js b/src/short-urls/SearchBar.js index 3c75178a..ea1c698e 100644 --- a/src/short-urls/SearchBar.js +++ b/src/short-urls/SearchBar.js @@ -7,23 +7,19 @@ import moment from 'moment'; import SearchField from '../utils/SearchField'; import Tag from '../tags/helpers/Tag'; import DateRangeRow from '../utils/DateRangeRow'; -import { compareVersions, formatDate } from '../utils/utils'; -import { serverType } from '../servers/prop-types'; +import { formatDate } from '../utils/utils'; import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; import './SearchBar.scss'; const propTypes = { listShortUrls: PropTypes.func, shortUrlsListParams: shortUrlsListParamsType, - selectedServer: serverType, }; const dateOrUndefined = (date) => date ? moment(date) : undefined; -const SearchBar = (colorGenerator) => { - const SearchBar = ({ listShortUrls, shortUrlsListParams, selectedServer }) => { - const currentServerVersion = selectedServer ? selectedServer.version : ''; - const enableDateFiltering = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '>=', '1.21.0'); +const SearchBar = (colorGenerator, ForServerVersion) => { + const SearchBar = ({ listShortUrls, shortUrlsListParams }) => { const selectedTags = shortUrlsListParams.tags || []; const setDate = (dateName) => pipe( formatDate(), @@ -38,7 +34,7 @@ const SearchBar = (colorGenerator) => { } /> - {enableDateFiltering && ( +
{ onEndDateChange={setDate('endDate')} />
- )} +
{!isEmpty(selectedTags) && (

diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index 29d67d43..dde64ad3 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -13,9 +13,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Link } from 'react-router-dom'; import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import PropTypes from 'prop-types'; -import { isEmpty } from 'ramda'; import { serverType } from '../../servers/prop-types'; -import { compareVersions } from '../../utils/utils'; import { shortUrlType } from '../reducers/shortUrlsList'; import PreviewModal from './PreviewModal'; import QrCodeModal from './QrCodeModal'; @@ -24,7 +22,8 @@ import './ShortUrlsRowMenu.scss'; const ShortUrlsRowMenu = ( DeleteShortUrlModal, EditTagsModal, - EditMetaModal + EditMetaModal, + ForServerVersion ) => class ShortUrlsRowMenu extends React.Component { static propTypes = { onCopyToClipboard: PropTypes.func, @@ -45,9 +44,6 @@ const ShortUrlsRowMenu = ( render() { const { onCopyToClipboard, shortUrl, selectedServer } = this.props; const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : ''; - const currentServerVersion = selectedServer ? selectedServer.version : ''; - const showEditMetaBtn = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '>=', '1.18.0'); - const showPreviewBtn = !isEmpty(currentServerVersion) && compareVersions(currentServerVersion, '<', '2.0.0'); const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] })); const toggleQrCode = toggleModal('isQrModalOpen'); const togglePreview = toggleModal('isPreviewModalOpen'); @@ -70,14 +66,12 @@ const ShortUrlsRowMenu = ( - {showEditMetaBtn && ( - - - Edit metadata - - - - )} + + + Edit metadata + + + Delete short URL @@ -86,21 +80,21 @@ const ShortUrlsRowMenu = ( - {showPreviewBtn && ( - - - Preview - - - - )} + + + Preview + + + QR code - {showPreviewBtn && } + + + diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js index 610d3e08..33c1ec26 100644 --- a/src/short-urls/services/provideServices.js +++ b/src/short-urls/services/provideServices.js @@ -24,8 +24,8 @@ const provideServices = (bottle, connect) => { (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) )); - bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); - bottle.decorator('SearchBar', connect([ 'shortUrlsListParams', 'selectedServer' ], [ 'listShortUrls' ])); + bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion'); + bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ])); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); bottle.decorator('ShortUrlsList', connect( @@ -35,10 +35,17 @@ const provideServices = (bottle, connect) => { bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'stateFlagTimeout'); - bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal', 'EditMetaModal'); + bottle.serviceFactory( + 'ShortUrlsRowMenu', + ShortUrlsRowMenu, + 'DeleteShortUrlModal', + 'EditTagsModal', + 'EditMetaModal', + 'ForServerVersion' + ); bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'stateFlagTimeout'); - bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult'); + bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult', 'ForServerVersion'); bottle.decorator( 'CreateShortUrl', connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ]) diff --git a/src/utils/ForVersion.js b/src/utils/ForVersion.js deleted file mode 100644 index 7ed39890..00000000 --- a/src/utils/ForVersion.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { isEmpty } from 'ramda'; -import { compareVersions } from './utils'; - -const propTypes = { - minVersion: PropTypes.string.isRequired, - currentServerVersion: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -}; - -const ForVersion = ({ minVersion, currentServerVersion, children }) => - isEmpty(currentServerVersion) || compareVersions(currentServerVersion, '<', minVersion) - ? null - : {children}; - -ForVersion.propTypes = propTypes; - -export default ForVersion; diff --git a/test/servers/helpers/ForServerVersion.test.js b/test/servers/helpers/ForServerVersion.test.js new file mode 100644 index 00000000..c229642a --- /dev/null +++ b/test/servers/helpers/ForServerVersion.test.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import each from 'jest-each'; +import ForServerVersion from '../../../src/servers/helpers/ForServerVersion'; + +describe('', () => { + let wrapped; + + const renderComponent = (minVersion, maxVersion, selectedServer) => { + wrapped = mount( + + Hello + + ); + + return wrapped; + }; + + afterEach(() => wrapped && wrapped.unmount()); + + it('does not render children when current server is empty', () => { + const wrapped = renderComponent('1'); + + expect(wrapped.html()).toBeNull(); + }); + + each([ + [ '2.0.0', undefined, '1.8.3' ], + [ undefined, '1.8.0', '1.8.3' ], + [ '1.7.0', '1.8.0', '1.8.3' ], + ]).it('does not render children when current version does not match requirements', (min, max, version) => { + const wrapped = renderComponent(min, max, { version }); + + expect(wrapped.html()).toBeNull(); + }); + + each([ + [ '2.0.0', undefined, '2.8.3' ], + [ '2.0.0', undefined, '2.0.0' ], + [ undefined, '1.8.0', '1.8.0' ], + [ undefined, '1.8.0', '1.7.1' ], + [ '1.7.0', '1.8.0', '1.7.3' ], + ]).it('renders children when current version matches requirements', (min, max, version) => { + const wrapped = renderComponent(min, max, { version }); + + expect(wrapped.html()).toContain('Hello'); + }); +}); diff --git a/test/short-urls/SearchBar.test.js b/test/short-urls/SearchBar.test.js index 172b8189..423af3cd 100644 --- a/test/short-urls/SearchBar.test.js +++ b/test/short-urls/SearchBar.test.js @@ -22,15 +22,10 @@ describe('', () => { expect(wrapper.find(SearchField)).toHaveLength(1); }); - each([ - [ '2.0.0', 1 ], - [ '1.21.2', 1 ], - [ '1.21.0', 1 ], - [ '1.20.0', 0 ], - ]).it('renders a DateRangeRow when proper version is run', (version, expectedLength) => { - wrapper = shallow(); + it('renders a DateRangeRow', () => { + wrapper = shallow(); - expect(wrapper.find(DateRangeRow)).toHaveLength(expectedLength); + expect(wrapper.find(DateRangeRow)).toHaveLength(1); }); it('renders no tags when the list of tags is empty', () => { @@ -69,7 +64,7 @@ describe('', () => { each([ 'startDateChange', 'endDateChange' ]).it('updates short URLs list when date range changes', (event) => { wrapper = shallow( - + ); const dateRange = wrapper.find(DateRangeRow); diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.js index 362d61e5..38d1db67 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.js +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.js @@ -1,7 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ButtonDropdown, DropdownItem } from 'reactstrap'; -import each from 'jest-each'; import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; import PreviewModal from '../../../src/short-urls/helpers/PreviewModal'; import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; @@ -17,12 +16,12 @@ describe('', () => { shortCode: 'abc123', shortUrl: 'https://doma.in/abc123', }; - const createWrapper = (serverVersion = '1.21.1') => { + const createWrapper = () => { const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, EditTagsModal, EditMetaModal); wrapper = shallow( @@ -46,24 +45,12 @@ describe('', () => { expect(qrCodeModal).toHaveLength(1); }); - each([ - [ '1.17.0', 6, 2 ], - [ '1.17.2', 6, 2 ], - [ '1.18.0', 7, 2 ], - [ '1.18.1', 7, 2 ], - [ '1.19.0', 7, 2 ], - [ '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); + it('renders correct amount of menu items', () => { + const wrapper = createWrapper(); const items = wrapper.find(DropdownItem); - expect(items).toHaveLength(expectedNonDividerItems + expectedDividerItems); - expect(items.find('[divider]')).toHaveLength(expectedDividerItems); + expect(items).toHaveLength(9); + expect(items.find('[divider]')).toHaveLength(2); }); describe('toggles state when toggling modal windows', () => { diff --git a/test/utils/ForVersion.test.js b/test/utils/ForVersion.test.js deleted file mode 100644 index 83be6fc5..00000000 --- a/test/utils/ForVersion.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import ForVersion from '../../src/utils/ForVersion'; - -describe('', () => { - let wrapped; - - const renderComponent = (minVersion, currentServerVersion) => { - wrapped = mount( - - Hello - - ); - - return wrapped; - }; - - afterEach(() => wrapped && wrapped.unmount()); - - it('does not render children when current version is empty', () => { - const wrapped = renderComponent('1', ''); - - expect(wrapped.html()).toBeNull(); - }); - - it('does not render children when current version is lower than min version', () => { - const wrapped = renderComponent('2.0.0', '1.8.3'); - - expect(wrapped.html()).toBeNull(); - }); - - it('renders children when current version is equal min version', () => { - const wrapped = renderComponent('2.0.0', '2.0.0'); - - expect(wrapped.html()).toContain('Hello'); - }); - - it('renders children when current version is higher than min version', () => { - const wrapped = renderComponent('2.0.0', '2.1.0'); - - expect(wrapped.html()).toContain('Hello'); - }); -});