Loaded version of selected server and created component to filter content based on that version

This commit is contained in:
Alejandro Celaya 2019-10-05 10:20:33 +02:00
parent 67a23bfe33
commit 4120d09220
11 changed files with 71 additions and 21 deletions

5
package-lock.json generated
View file

@ -4951,6 +4951,11 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.5.1.tgz",
"integrity": "sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg=="
},
"component-emitter": { "component-emitter": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz", "resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz",

View file

@ -35,6 +35,7 @@
"bottlejs": "^1.7.1", "bottlejs": "^1.7.1",
"chart.js": "^2.7.2", "chart.js": "^2.7.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"compare-versions": "^3.5.1",
"csvjson": "^5.1.0", "csvjson": "^5.1.0",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
"moment": "^2.22.2", "moment": "^2.22.2",

View file

@ -10,14 +10,19 @@ const initialState = null;
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER); export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
export const selectServer = ({ findServerById }) => (serverId) => (dispatch) => { export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serverId) => async (dispatch) => {
dispatch(resetShortUrlParams()); dispatch(resetShortUrlParams());
const selectedServer = findServerById(serverId); const selectedServer = findServerById(serverId);
const { health } = await buildShlinkApiClient(selectedServer);
const { version } = await health();
dispatch({ dispatch({
type: SELECT_SERVER, type: SELECT_SERVER,
selectedServer, selectedServer: {
...selectedServer,
version,
},
}); });
}; };

View file

@ -34,7 +34,7 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
// Actions // Actions
bottle.serviceFactory('selectServer', selectServer, 'ServersService'); bottle.serviceFactory('selectServer', selectServer, 'ServersService', 'buildShlinkApiClient');
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers'); bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers'); bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers'); bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');

View file

@ -6,6 +6,8 @@ import { Collapse } from 'reactstrap';
import * as PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import DateInput from '../utils/DateInput'; import DateInput from '../utils/DateInput';
import Checkbox from '../utils/Checkbox'; import Checkbox from '../utils/Checkbox';
import ForVersion from '../utils/ForVersion';
import { serverType } from '../servers/prop-types';
import { createShortUrlResultType } from './reducers/shortUrlCreation'; import { createShortUrlResultType } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon'; import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
@ -17,6 +19,7 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort
createShortUrl: PropTypes.func, createShortUrl: PropTypes.func,
shortUrlCreationResult: createShortUrlResultType, shortUrlCreationResult: createShortUrlResultType,
resetCreateShortUrl: PropTypes.func, resetCreateShortUrl: PropTypes.func,
selectedServer: serverType,
}; };
state = { state = {
@ -108,16 +111,21 @@ const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShort
</div> </div>
</div> </div>
<div className="mb-4 text-right"> <ForVersion
<Checkbox minVersion="1.16.0"
className="mr-2" currentServerVersion={this.props.selectedServer ? this.props.selectedServer.version : ''}
checked={this.state.findIfExists} >
onChange={(findIfExists) => this.setState({ findIfExists })} <div className="mb-4 text-right">
> <Checkbox
Use existing URL if found className="mr-2"
</Checkbox> checked={this.state.findIfExists}
<UseExistingIfFoundInfoIcon /> onChange={(findIfExists) => this.setState({ findIfExists })}
</div> >
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForVersion>
</Collapse> </Collapse>
<div> <div>

View file

@ -23,8 +23,8 @@ const renderInfoModal = (isOpen, toggle) => (
if none is found. if none is found.
</li> </li>
<li> <li>
When long URL and custom slug are provided: Same as in previous case, but it will try to match the short URL When long URL and custom slug and/or domain are provided: Same as in previous case, but it will try to match
using both the long URL and the slug. the short URL using both the long URL and the slug, the long URL and the domain, or the three of them.
<br /> <br />
If the slug is being used by another long URL, an error will be returned. If the slug is being used by another long URL, an error will be returned.
</li> </li>

View file

@ -39,7 +39,7 @@ const provideServices = (bottle, connect) => {
bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult'); bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector', 'CreateShortUrlResult');
bottle.decorator( bottle.decorator(
'CreateShortUrl', 'CreateShortUrl',
connect([ 'shortUrlCreationResult' ], [ 'createShortUrl', 'resetCreateShortUrl' ]) connect([ 'shortUrlCreationResult', 'selectedServer' ], [ 'createShortUrl', 'resetCreateShortUrl' ])
); );
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);

19
src/utils/ForVersion.js Normal file
View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compare } from 'compare-versions';
import { isEmpty } from 'ramda';
const propTypes = {
minVersion: PropTypes.string.isRequired,
currentServerVersion: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
const ForVersion = ({ minVersion, currentServerVersion, children }) =>
isEmpty(currentServerVersion) || compare(minVersion, currentServerVersion, '>')
? null
: <React.Fragment>{children}</React.Fragment>;
ForVersion.propTypes = propTypes;
export default ForVersion;

View file

@ -50,6 +50,8 @@ export default class ShlinkApiClient {
this._performRequest('/tags', 'PUT', {}, { oldName, newName }) this._performRequest('/tags', 'PUT', {}, { oldName, newName })
.then(() => ({ oldName, newName })); .then(() => ({ oldName, newName }));
health = () => this._performRequest('/health', 'GET').then((resp) => resp.data);
_performRequest = async (url, method = 'GET', query = {}, body = {}) => _performRequest = async (url, method = 'GET', query = {}, body = {}) =>
await this.axios({ await this.axios({
method, method,

View file

@ -13,8 +13,10 @@ const getSelectedServerFromState = async (getState) => {
return selectedServer; return selectedServer;
}; };
const buildShlinkApiClient = (axios) => async (getState) => { const buildShlinkApiClient = (axios) => async (getStateOrSelectedServer) => {
const { url, apiKey } = await getSelectedServerFromState(getState); const { url, apiKey } = typeof getStateOrSelectedServer === 'function'
? await getSelectedServerFromState(getStateOrSelectedServer)
: getStateOrSelectedServer;
const clientKey = `${url}_${apiKey}`; const clientKey = `${url}_${apiKey}`;
if (!apiClients[clientKey]) { if (!apiClients[clientKey]) {

View file

@ -29,22 +29,30 @@ describe('selectedServerReducer', () => {
const selectedServer = { const selectedServer = {
id: serverId, id: serverId,
}; };
const version = '1.19.0';
const ServersServiceMock = { const ServersServiceMock = {
findServerById: jest.fn(() => selectedServer), findServerById: jest.fn(() => selectedServer),
}; };
const apiClientMock = {
health: jest.fn().mockResolvedValue({ version }),
};
afterEach(() => { afterEach(() => {
ServersServiceMock.findServerById.mockClear(); ServersServiceMock.findServerById.mockClear();
}); });
it('dispatches proper actions', () => { it('dispatches proper actions', async () => {
const dispatch = jest.fn(); const dispatch = jest.fn();
const expectedSelectedServer = {
...selectedServer,
version,
};
selectServer(ServersServiceMock)(serverId)(dispatch); await selectServer(ServersServiceMock, async () => apiClientMock)(serverId)(dispatch);
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SHORT_URL_PARAMS }); expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SHORT_URL_PARAMS });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer }); expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
}); });
it('invokes dependencies', () => { it('invokes dependencies', () => {