From 7637ce3107f2a3905145774c2fd42a94941e47be Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 22 Oct 2021 20:13:23 +0200 Subject: [PATCH] Added logic to toggle auto-connect on servers --- src/servers/ManageServers.tsx | 4 +- src/servers/ManageServersRow.tsx | 20 ++++--- src/servers/ServersDropdown.tsx | 9 +-- src/servers/reducers/servers.ts | 40 +++++++++++-- src/servers/services/provideServices.ts | 5 +- test/servers/reducers/servers.test.ts | 77 ++++++++++++++++++++++++- 6 files changed, 132 insertions(+), 23 deletions(-) diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index ad1195d0..9554f2e8 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -57,11 +57,11 @@ export const ManageServers = ( - + - {hasAutoConnect && {hasAutoConnect && ( - @@ -53,7 +59,7 @@ export const ManageServersRow = ( Edit server - + setAutoConnect(server, !server.autoConnect)}> {isAutoConnect ? 'Do not a' : 'A'}uto-connect diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index 6adb05b3..c109d7bd 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -3,7 +3,7 @@ import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from import { Link } from 'react-router-dom'; import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isServerWithId, SelectedServer, ServersMap } from './data'; +import { getServerId, SelectedServer, ServersMap } from './data'; export interface ServersDropdownProps { servers: ServersMap; @@ -25,12 +25,7 @@ const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => { return ( <> {serversList.map(({ name, id }) => ( - + {name} ))} diff --git a/src/servers/reducers/servers.ts b/src/servers/reducers/servers.ts index 057f5531..e433b9e1 100644 --- a/src/servers/reducers/servers.ts +++ b/src/servers/reducers/servers.ts @@ -1,4 +1,4 @@ -import { assoc, dissoc, map, pipe, reduce } from 'ramda'; +import { assoc, dissoc, fromPairs, map, pipe, reduce, toPairs } from 'ramda'; import { v4 as uuid } from 'uuid'; import { Action } from 'redux'; import { ServerData, ServersMap, ServerWithId } from '../data'; @@ -8,12 +8,22 @@ import { buildReducer } from '../../utils/helpers/redux'; export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER'; export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER'; export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS'; +export const SET_AUTO_CONNECT = 'shlink/servers/SET_AUTO_CONNECT'; /* eslint-enable padding-line-between-statements */ export interface CreateServersAction extends Action { newServers: ServersMap; } +interface DeleteServerAction extends Action { + serverId: string; +} + +interface SetAutoConnectAction extends Action { + serverId: string; + autoConnect: boolean; +} + const initialState: ServersMap = {}; const serverWithId = (server: ServerWithId | ServerData): ServerWithId => { @@ -24,12 +34,28 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => { return assoc('id', uuid(), server); }; -export default buildReducer({ +export default buildReducer({ [CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }), - [DELETE_SERVER]: (state, { serverId }: any) => dissoc(serverId, state), + [DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state), [EDIT_SERVER]: (state, { serverId, serverData }: any) => !state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state), + [SET_AUTO_CONNECT]: (state, { serverId, autoConnect }) => { + if (!state[serverId]) { + return state; + } + + if (!autoConnect) { + return assoc(serverId, { ...state[serverId], autoConnect }, state); + } + + return fromPairs( + toPairs(state).map(([ evaluatedServerId, server ]) => [ + evaluatedServerId, + { ...server, autoConnect: evaluatedServerId === serverId }, + ]), + ); + }, }, initialState); const serversListToMap = reduce((acc, server) => assoc(server.id, server, acc), {}); @@ -48,4 +74,10 @@ export const editServer = (serverId: string, serverData: Partial) => serverData, }); -export const deleteServer = ({ id }: ServerWithId) => ({ type: DELETE_SERVER, serverId: id }); +export const deleteServer = ({ id }: ServerWithId): DeleteServerAction => ({ type: DELETE_SERVER, serverId: id }); + +export const setAutoConnect = ({ id }: ServerWithId, autoConnect: boolean): SetAutoConnectAction => ({ + type: SET_AUTO_CONNECT, + serverId: id, + autoConnect, +}); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index bea4bd94..afa4cd76 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -7,7 +7,7 @@ import DeleteServerButton from '../DeleteServerButton'; import { EditServer } from '../EditServer'; import ImportServersBtn from '../helpers/ImportServersBtn'; import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; -import { createServer, createServers, deleteServer, editServer } from '../reducers/servers'; +import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers'; import { fetchServers } from '../reducers/remoteServers'; import ForServerVersion from '../helpers/ForServerVersion'; import { ServerError } from '../helpers/ServerError'; @@ -32,7 +32,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: bottle.decorator('ManageServers', connect([ 'servers' ])); bottle.serviceFactory('ManageServersRow', ManageServersRow, 'DeleteServerModal'); - bottle.decorator('ManageServers', connect([ 'servers' ])); + bottle.decorator('ManageServersRow', connect(null, [ 'setAutoConnect' ])); bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout'); bottle.decorator('CreateServer', withoutSelectedServer); @@ -77,6 +77,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: bottle.serviceFactory('createServers', () => createServers); bottle.serviceFactory('deleteServer', () => deleteServer); bottle.serviceFactory('editServer', () => editServer); + bottle.serviceFactory('setAutoConnect', () => setAutoConnect); bottle.serviceFactory('fetchServers', fetchServers, 'axios'); bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer); diff --git a/test/servers/reducers/servers.test.ts b/test/servers/reducers/servers.test.ts index e4ad7e58..c781eb54 100644 --- a/test/servers/reducers/servers.test.ts +++ b/test/servers/reducers/servers.test.ts @@ -1,13 +1,15 @@ -import { values } from 'ramda'; +import { dissoc, values } from 'ramda'; import { Mock } from 'ts-mockery'; import reducer, { createServer, deleteServer, createServers, editServer, + setAutoConnect, EDIT_SERVER, DELETE_SERVER, CREATE_SERVERS, + SET_AUTO_CONNECT, } from '../../../src/servers/reducers/servers'; import { RegularServer } from '../../../src/servers/data'; @@ -29,6 +31,15 @@ describe('serverReducer', () => { def456: { id: 'def456' }, })); + it('returns as it is when action is EDIT_SERVER and server does not exist', () => + expect(reducer( + list, + { type: EDIT_SERVER, serverId: 'invalid', serverData: { foo: 'foo' } } as any, + )).toEqual({ + abc123: { id: 'abc123' }, + def456: { id: 'def456' }, + })); + it('removes server when action is DELETE_SERVER', () => expect(reducer(list, { type: DELETE_SERVER, serverId: 'abc123' } as any)).toEqual({ def456: { id: 'def456' }, @@ -45,6 +56,51 @@ describe('serverReducer', () => { def456: { id: 'def456' }, ghi789: { id: 'ghi789' }, })); + + it.each([ + [ true ], + [ false ], + ])('returns state as it is when trying to set auto-connect on invalid server', (autoConnect) => + expect(reducer(list, { + type: SET_AUTO_CONNECT, + serverId: 'invalid', + autoConnect, + } as any)).toEqual({ + abc123: { id: 'abc123' }, + def456: { id: 'def456' }, + })); + + it('disables auto-connect on a server which is already set to auto-connect', () => { + const listWithDisabledAutoConnect = { + ...list, + abc123: { ...list.abc123, autoConnect: true }, + }; + + expect(reducer(listWithDisabledAutoConnect, { + type: SET_AUTO_CONNECT, + serverId: 'abc123', + autoConnect: false, + } as any)).toEqual({ + abc123: { id: 'abc123', autoConnect: false }, + def456: { id: 'def456' }, + }); + }); + + it('disables auto-connect on all servers except selected one', () => { + const listWithEnabledAutoConnect = { + ...list, + abc123: { ...list.abc123, autoConnect: true }, + }; + + expect(reducer(listWithEnabledAutoConnect, { + type: SET_AUTO_CONNECT, + serverId: 'def456', + autoConnect: true, + } as any)).toEqual({ + abc123: { id: 'abc123', autoConnect: false }, + def456: { id: 'def456', autoConnect: true }, + }); + }); }); describe('action creators', () => { @@ -82,6 +138,25 @@ describe('serverReducer', () => { expect(result).toEqual(expect.objectContaining({ type: CREATE_SERVERS })); }); + + it('generates an id for every provided server if they do not have it', () => { + const servers = values(list).map(dissoc('id')); + const { newServers } = createServers(servers); + + expect(values(newServers).every(({ id }) => !!id)).toEqual(true); + }); + }); + + describe('setAutoConnect', () => { + it.each([ + [ true ], + [ false ], + ])('returns expected action', (autoConnect) => { + const serverToEdit = Mock.of({ id: 'abc123' }); + const result = setAutoConnect(serverToEdit, autoConnect); + + expect(result).toEqual({ type: SET_AUTO_CONNECT, serverId: 'abc123', autoConnect }); + }); }); }); });
} + {hasAutoConnect && } Name Base URL diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx index 03ca9cae..d3f70483 100644 --- a/src/servers/ManageServersRow.tsx +++ b/src/servers/ManageServersRow.tsx @@ -7,9 +7,9 @@ import { faEdit as editIcon, faMinusCircle as deleteIcon, faPlug as connectIcon, - faToggleOn as toggleOnIcon, - faToggleOff as toggleOffIcon, + faBan as toggleOffIcon, } from '@fortawesome/free-solid-svg-icons'; +import { faCircle as toggleOnIcon } from '@fortawesome/free-regular-svg-icons'; import { DropdownBtnMenu } from '../utils/DropdownBtnMenu'; import { useToggle } from '../utils/helpers/hooks'; import { ServerWithId } from './data'; @@ -20,23 +20,29 @@ export interface ManageServersRowProps { hasAutoConnect: boolean; } +interface ManageServersRowPropsConnectProps extends ManageServersRowProps { + setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void; +} + export const ManageServersRow = ( DeleteServerModal: FC, -): FC => ({ server, hasAutoConnect }) => { +): FC => ({ server, hasAutoConnect, setAutoConnect }) => { const [ isMenuOpen, toggleMenu ] = useToggle(); const [ isModalOpen,, showModal, hideModal ] = useToggle(); const serverUrl = `/server/${server.id}`; const { autoConnect: isAutoConnect } = server; - const autoConnectIcon = isAutoConnect ? toggleOnIcon : toggleOffIcon; + const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon; return (
+ {isAutoConnect && ( <> - Auto-connect to this server + + Auto-connect to this server + )}