diff --git a/.eslintrc b/.eslintrc index 78ada1c2..48aab946 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ }, "ignorePatterns": ["src/service*.ts"], "rules": { - "complexity": "off" + "complexity": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index a03fa78c..55c3f84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added * [#496](https://github.com/shlinkio/shlink-web-client/issues/496) Allowed to select "all visits" as the default interval for visits. * [#500](https://github.com/shlinkio/shlink-web-client/issues/500) Allowed to set the `forwardQuery` flag when creating/editing short URLs on a Shlink v2.9.0 server. +* [#508](https://github.com/shlinkio/shlink-web-client/issues/508) Added new servers management section. ### Changed * Moved ci workflow to external repo and reused diff --git a/src/app/App.tsx b/src/app/App.tsx index d8b91056..0f43ab29 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -23,6 +23,7 @@ const App = ( CreateServer: FC, EditServer: FC, Settings: FC, + ManageServers: FC, ShlinkVersionsContainer: FC, ) => ({ fetchServers, servers, settings, appUpdated, resetAppUpdate }: AppProps) => { useEffect(() => { @@ -43,6 +44,7 @@ const App = ( + diff --git a/src/app/services/provideServices.ts b/src/app/services/provideServices.ts index 4dff9f31..50bb8f12 100644 --- a/src/app/services/provideServices.ts +++ b/src/app/services/provideServices.ts @@ -14,6 +14,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 'CreateServer', 'EditServer', 'Settings', + 'ManageServers', 'ShlinkVersionsContainer', ); bottle.decorator('App', connect([ 'servers', 'settings', 'appUpdated' ], [ 'fetchServers', 'resetAppUpdate' ])); diff --git a/src/index.scss b/src/index.scss index 419f1638..f1cee307 100644 --- a/src/index.scss +++ b/src/index.scss @@ -115,6 +115,16 @@ hr { color: var(--text-color) !important; } +.dropdown-item--danger.dropdown-item--danger { + color: $dangerColor; + + &:hover, + &:active, + &.active { + color: $dangerColor !important; + } +} + .badge-main { color: #ffffff; background-color: var(--brand-color); diff --git a/src/servers/CreateServer.scss b/src/servers/CreateServer.scss index cfba848d..4861c9af 100644 --- a/src/servers/CreateServer.scss +++ b/src/servers/CreateServer.scss @@ -8,9 +8,3 @@ text-align: right; } } - -.create-server__csv-select { - position: absolute; - left: -9999px; - top: -9999px; -} diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 0f87fa0d..e24e548c 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,30 +1,35 @@ import { FC } from 'react'; import { v4 as uuid } from 'uuid'; import { RouterProps } from 'react-router'; +import { Button } from 'reactstrap'; import { Result } from '../utils/Result'; import NoMenuLayout from '../common/NoMenuLayout'; import { StateFlagTimeout } from '../utils/helpers/hooks'; import { ServerForm } from './helpers/ServerForm'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; -import { ServerData, ServerWithId } from './data'; +import { ServerData, ServersMap, ServerWithId } from './data'; import './CreateServer.scss'; const SHOW_IMPORT_MSG_TIME = 4000; interface CreateServerProps extends RouterProps { createServer: (server: ServerWithId) => void; + servers: ServersMap; } const ImportResult = ({ type }: { type: 'error' | 'success' }) => ( - - {type === 'success' && 'Servers properly imported. You can now select one from the list :)'} - {type === 'error' && 'The servers could not be imported. Make sure the format is correct.'} - +
+ + {type === 'success' && 'Servers properly imported. You can now select one from the list :)'} + {type === 'error' && 'The servers could not be imported. Make sure the format is correct.'} + +
); const CreateServer = (ImportServersBtn: FC, useStateFlagTimeout: StateFlagTimeout) => ( - { createServer, history: { push } }: CreateServerProps, + { servers, createServer, history: { push, goBack } }: CreateServerProps, ) => { + const hasServers = !!Object.keys(servers).length; const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); const handleSubmit = (serverData: ServerData) => { @@ -37,16 +42,14 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT return ( Add new server} onSubmit={handleSubmit}> - - + {!hasServers && + } + {hasServers && } + - {(serversImported || errorImporting) && ( -
- {serversImported && } - {errorImporting && } -
- )} + {serversImported && } + {errorImporting && }
); }; diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index 4d74869e..491d2f95 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -1,3 +1,4 @@ +import { FC } from 'react'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { RouterProps } from 'react-router'; import { ServerWithId } from './data'; @@ -6,17 +7,20 @@ export interface DeleteServerModalProps { server: ServerWithId; toggle: () => void; isOpen: boolean; + redirectHome?: boolean; } interface DeleteServerModalConnectProps extends DeleteServerModalProps, RouterProps { deleteServer: (server: ServerWithId) => void; } -const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }: DeleteServerModalConnectProps) => { +const DeleteServerModal: FC = ( + { server, toggle, isOpen, deleteServer, history, redirectHome = true }, +) => { const closeModal = () => { deleteServer(server); toggle(); - history.push('/'); + redirectHome && history.push('/'); }; return ( diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index f18c788d..f6576066 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -10,7 +10,7 @@ interface EditServerProps { } export const EditServer = (ServerError: FC) => withSelectedServer(( - { editServer, selectedServer, history: { push, goBack } }, + { editServer, selectedServer, history: { goBack } }, ) => { if (!isServerWithId(selectedServer)) { return null; @@ -18,7 +18,7 @@ export const EditServer = (ServerError: FC) => withSelectedServer { editServer(selectedServer.id, serverData); - push(`/server/${selectedServer.id}`); + goBack(); }; return ( diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx new file mode 100644 index 00000000..9554f2e8 --- /dev/null +++ b/src/servers/ManageServers.tsx @@ -0,0 +1,86 @@ +import { FC, useEffect, useState } from 'react'; +import { Button, Row } from 'reactstrap'; +import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Link } from 'react-router-dom'; +import NoMenuLayout from '../common/NoMenuLayout'; +import { SimpleCard } from '../utils/SimpleCard'; +import SearchField from '../utils/SearchField'; +import { Result } from '../utils/Result'; +import { StateFlagTimeout } from '../utils/helpers/hooks'; +import { ImportServersBtnProps } from './helpers/ImportServersBtn'; +import { ServersMap } from './data'; +import { ManageServersRowProps } from './ManageServersRow'; +import ServersExporter from './services/ServersExporter'; + +interface ManageServersProps { + servers: ServersMap; +} + +const SHOW_IMPORT_MSG_TIME = 4000; + +export const ManageServers = ( + serversExporter: ServersExporter, + ImportServersBtn: FC, + useStateFlagTimeout: StateFlagTimeout, + ManageServersRow: FC, +): FC => ({ servers }) => { + const allServers = Object.values(servers); + const [ serversList, setServersList ] = useState(allServers); + const filterServers = (searchTerm: string) => setServersList( + allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)), + ); + const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect); + const [ errorImporting, setErrorImporting ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); + + useEffect(() => { + setServersList(Object.values(servers)); + }, [ servers ]); + + return ( + + + + +
+ Import servers + {allServers.length > 0 && ( + + )} +
+
+ +
+
+ + + + + + {hasAutoConnect && + + + + + {!serversList.length && } + {serversList.map((server) => + ) + } + +
} + NameBase URL +
No servers found.
+
+ + {errorImporting && ( +
+ The servers could not be imported. Make sure the format is correct. +
+ )} +
+ ); +}; diff --git a/src/servers/ManageServersRow.tsx b/src/servers/ManageServersRow.tsx new file mode 100644 index 00000000..396c30e0 --- /dev/null +++ b/src/servers/ManageServersRow.tsx @@ -0,0 +1,38 @@ +import { FC } from 'react'; +import { UncontrolledTooltip } from 'reactstrap'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons'; +import { ServerWithId } from './data'; +import { ManageServersRowDropdownProps } from './ManageServersRowDropdown'; + +export interface ManageServersRowProps { + server: ServerWithId; + hasAutoConnect: boolean; +} + +export const ManageServersRow = ( + ManageServersRowDropdown: FC, +): FC => ({ server, hasAutoConnect }) => ( + + {hasAutoConnect && ( + + {server.autoConnect && ( + <> + + + Auto-connect to this server + + + )} + + )} + + {server.name} + + {server.url} + + + + +); diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx new file mode 100644 index 00000000..7c85e49f --- /dev/null +++ b/src/servers/ManageServersRowDropdown.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import { DropdownItem } from 'reactstrap'; +import { Link } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faBan as toggleOffIcon, + faEdit as editIcon, + faMinusCircle as deleteIcon, + faPlug as connectIcon, +} 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 { DeleteServerModalProps } from './DeleteServerModal'; +import { ServerWithId } from './data'; + +export interface ManageServersRowDropdownProps { + server: ServerWithId; +} + +interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownProps { + setAutoConnect: (server: ServerWithId, autoConnect: boolean) => void; +} + +export const ManageServersRowDropdown = ( + DeleteServerModal: FC, +): FC => ({ server, setAutoConnect }) => { + const [ isMenuOpen, toggleMenu ] = useToggle(); + const [ isModalOpen,, showModal, hideModal ] = useToggle(); + const serverUrl = `/server/${server.id}`; + const { autoConnect: isAutoConnect } = server; + const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon; + + return ( + + + Connect + + + Edit server + + setAutoConnect(server, !server.autoConnect)}> + {isAutoConnect ? 'Do not a' : 'A'}uto-connect + + + + Remove server + + + + + ); +}; diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index 284643f6..c109d7bd 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -1,45 +1,37 @@ import { isEmpty, values } from 'ramda'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import { Link } from 'react-router-dom'; -import { faPlus as plusIcon, faFileDownload as exportIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons'; +import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import ServersExporter from './services/ServersExporter'; -import { isServerWithId, SelectedServer, ServersMap } from './data'; +import { getServerId, SelectedServer, ServersMap } from './data'; export interface ServersDropdownProps { servers: ServersMap; selectedServer: SelectedServer; } -const ServersDropdown = (serversExporter: ServersExporter) => ({ servers, selectedServer }: ServersDropdownProps) => { +const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => { const serversList = values(servers); - const createServerItem = ( - - Add a server - - ); const renderServers = () => { if (isEmpty(serversList)) { - return createServerItem; + return ( + + Add a server + + ); } return ( <> {serversList.map(({ name, id }) => ( - + {name} ))} - {createServerItem} - serversExporter.exportServers()}> - Export servers + + Manage servers ); diff --git a/src/servers/data/index.ts b/src/servers/data/index.ts index 6edee8ec..809a6636 100644 --- a/src/servers/data/index.ts +++ b/src/servers/data/index.ts @@ -8,6 +8,7 @@ export interface ServerData { export interface ServerWithId extends ServerData { id: string; + autoConnect?: boolean; } export interface ReachableServer extends ServerWithId { diff --git a/src/servers/helpers/ImportServersBtn.scss b/src/servers/helpers/ImportServersBtn.scss new file mode 100644 index 00000000..32b254a8 --- /dev/null +++ b/src/servers/helpers/ImportServersBtn.scss @@ -0,0 +1,5 @@ +.import-servers-btn__csv-select { + position: absolute; + left: -9999px; + top: -9999px; +} diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 490d2d7e..98175a87 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -1,13 +1,18 @@ -import { useRef, RefObject, ChangeEvent, MutableRefObject } from 'react'; -import { UncontrolledTooltip } from 'reactstrap'; +import { useRef, RefObject, ChangeEvent, MutableRefObject, FC } from 'react'; +import { Button, UncontrolledTooltip } from 'reactstrap'; +import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import ServersImporter from '../services/ServersImporter'; import { ServerData } from '../data'; +import './ImportServersBtn.scss'; type Ref = RefObject | MutableRefObject; export interface ImportServersBtnProps { onImport?: () => void; onImportError?: (error: Error) => void; + tooltipPlacement?: 'top' | 'bottom'; + className?: string; } interface ImportServersBtnConnectProps extends ImportServersBtnProps { @@ -15,12 +20,15 @@ interface ImportServersBtnConnectProps extends ImportServersBtnProps { fileRef: Ref; } -const ImportServersBtn = ({ importServersFromFile }: ServersImporter) => ({ +const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC => ({ createServers, fileRef, + children, onImport = () => {}, onImportError = () => {}, -}: ImportServersBtnConnectProps) => { + tooltipPlacement = 'bottom', + className = '', +}) => { const ref = fileRef ?? useRef(); const onChange = async ({ target }: ChangeEvent) => importServersFromFile(target.files?.[0]) @@ -34,19 +42,14 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter) => ({ return ( <> - - + + You can create servers by importing a CSV file with columns name, apiKey and url. - + ); }; diff --git a/src/servers/helpers/ServerForm.tsx b/src/servers/helpers/ServerForm.tsx index 3d30b926..9370cbb3 100644 --- a/src/servers/helpers/ServerForm.tsx +++ b/src/servers/helpers/ServerForm.tsx @@ -31,7 +31,7 @@ export const ServerForm: FC = ({ onSubmit, initialValues, child Name URL - APIkey + API key
{children}
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 14453446..07dd9361 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -7,26 +7,44 @@ 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'; import { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../helpers/withoutSelectedServer'; import { Overview } from '../Overview'; +import { ManageServers } from '../ManageServers'; +import { ManageServersRow } from '../ManageServersRow'; +import { ManageServersRowDropdown } from '../ManageServersRowDropdown'; import ServersImporter from './ServersImporter'; import ServersExporter from './ServersExporter'; const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: Decorator) => { // Components + bottle.serviceFactory( + 'ManageServers', + ManageServers, + 'ServersExporter', + 'ImportServersBtn', + 'useStateFlagTimeout', + 'ManageServersRow', + ); + bottle.decorator('ManageServers', connect([ 'servers' ])); + + bottle.serviceFactory('ManageServersRow', ManageServersRow, 'ManageServersRowDropdown'); + + bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal'); + bottle.decorator('ManageServersRowDropdown', connect(null, [ 'setAutoConnect' ])); + bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout'); bottle.decorator('CreateServer', withoutSelectedServer); - bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ])); + bottle.decorator('CreateServer', connect([ 'selectedServer', 'servers' ], [ 'createServer', 'resetSelectedServer' ])); bottle.serviceFactory('EditServer', EditServer, 'ServerError'); bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer' ])); - bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); + bottle.serviceFactory('ServersDropdown', () => ServersDropdown); bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ])); bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); @@ -62,6 +80,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/src/short-urls/helpers/ShortUrlsRowMenu.scss b/src/short-urls/helpers/ShortUrlsRowMenu.scss deleted file mode 100644 index cb60b9ae..00000000 --- a/src/short-urls/helpers/ShortUrlsRowMenu.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import '../../utils/base'; - -.short-urls-row-menu__dropdown-item--danger.short-urls-row-menu__dropdown-item--danger { - color: $dangerColor; - - &:hover, - &:active, - &.active { - color: $dangerColor !important; - } -} diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.tsx b/src/short-urls/helpers/ShortUrlsRowMenu.tsx index 2ea3b996..9b84501f 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.tsx +++ b/src/short-urls/helpers/ShortUrlsRowMenu.tsx @@ -12,7 +12,6 @@ import { ShortUrl, ShortUrlModalProps } from '../data'; import { SelectedServer } from '../../servers/data'; import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu'; import ShortUrlDetailLink from './ShortUrlDetailLink'; -import './ShortUrlsRowMenu.scss'; export interface ShortUrlsRowMenuProps { selectedServer: SelectedServer; @@ -45,7 +44,7 @@ const ShortUrlsRowMenu = ( - + Delete short URL diff --git a/src/utils/FormGroupContainer.tsx b/src/utils/FormGroupContainer.tsx index d522ebfb..767a4fbf 100644 --- a/src/utils/FormGroupContainer.tsx +++ b/src/utils/FormGroupContainer.tsx @@ -1,6 +1,7 @@ import { FC, useRef } from 'react'; import { v4 as uuid } from 'uuid'; import { InputType } from 'reactstrap/lib/Input'; +import { FormGroup } from 'reactstrap'; export interface FormGroupContainerProps { value: string; @@ -19,7 +20,7 @@ export const FormGroupContainer: FC = ( const forId = useRef(id ?? uuid()); return ( -
+ @@ -32,6 +33,6 @@ export const FormGroupContainer: FC = ( placeholder={placeholder} onChange={(e) => onChange(e.target.value)} /> -
+ ); }; diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 873e70ec..1638b6fc 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -11,7 +11,16 @@ describe('', () => { const ShlinkVersions = () => null; beforeEach(() => { - const App = appFactory(MainHeader, () => null, () => null, () => null, () => null, () => null, ShlinkVersions); + const App = appFactory( + MainHeader, + () => null, + () => null, + () => null, + () => null, + () => null, + () => null, + ShlinkVersions, + ); wrapper = shallow( ', () => { const expectedPaths = [ '/', '/settings', + '/manage-servers', '/server/create', '/server/:serverId/edit', '/server/:serverId', diff --git a/test/common/AppUpdateBanner.test.tsx b/test/common/AppUpdateBanner.test.tsx index b03458b8..6231e8e4 100644 --- a/test/common/AppUpdateBanner.test.tsx +++ b/test/common/AppUpdateBanner.test.tsx @@ -24,7 +24,7 @@ describe('', () => { }); it('invokes toggle when alert is toggled', () => { - (wrapper.prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (wrapper.prop('toggle') as Function)(); expect(toggle).toHaveBeenCalled(); }); diff --git a/test/domains/helpers/EditDomainRedirectsModal.test.tsx b/test/domains/helpers/EditDomainRedirectsModal.test.tsx index 2293a807..067f5f3b 100644 --- a/test/domains/helpers/EditDomainRedirectsModal.test.tsx +++ b/test/domains/helpers/EditDomainRedirectsModal.test.tsx @@ -42,7 +42,7 @@ describe('', () => { it('has different handlers to toggle the modal', () => { expect(toggle).not.toHaveBeenCalled(); - (wrapper.prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (wrapper.prop('toggle') as Function)(); (wrapper.find(ModalHeader).prop('toggle') as Function)(); wrapper.find(Button).first().simulate('click'); diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index dcc90006..7cb2e388 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -3,6 +3,7 @@ import { Mock } from 'ts-mockery'; import { History } from 'history'; import createServerConstruct from '../../src/servers/CreateServer'; import { ServerForm } from '../../src/servers/helpers/ServerForm'; +import { ServerWithId } from '../../src/servers/data'; describe('', () => { let wrapper: ShallowWrapper; @@ -10,13 +11,14 @@ describe('', () => { const createServerMock = jest.fn(); const push = jest.fn(); const historyMock = Mock.of({ push }); + const servers = { foo: Mock.all() }; const createWrapper = (serversImported = false, importFailed = false) => { const useStateFlagTimeout = jest.fn() .mockReturnValueOnce([ serversImported, () => '' ]) .mockReturnValueOnce([ importFailed, () => '' ]); const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout); - wrapper = shallow(); + wrapper = shallow(); return wrapper; }; diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index c74cb72c..4f6a65da 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -10,8 +10,8 @@ describe('', () => { let wrapper: ReactWrapper; const ServerError = jest.fn(); const editServerMock = jest.fn(); - const push = jest.fn(); - const historyMock = Mock.of({ push }); + const goBack = jest.fn(); + const historyMock = Mock.of({ goBack }); const match = Mock.of>({ params: { serverId: 'abc123' }, }); @@ -50,6 +50,6 @@ describe('', () => { form.simulate('submit', {}); expect(editServerMock).toHaveBeenCalledTimes(1); - expect(push).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledTimes(1); }); }); diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx new file mode 100644 index 00000000..c860bb9e --- /dev/null +++ b/test/servers/ManageServers.test.tsx @@ -0,0 +1,93 @@ +import { Mock } from 'ts-mockery'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { Button } from 'reactstrap'; +import ServersExporter from '../../src/servers/services/ServersExporter'; +import { ManageServers as createManageServers } from '../../src/servers/ManageServers'; +import { ServersMap, ServerWithId } from '../../src/servers/data'; +import SearchField from '../../src/utils/SearchField'; +import { Result } from '../../src/utils/Result'; + +describe('', () => { + const exportServers = jest.fn(); + const serversExporter = Mock.of({ exportServers }); + const ImportServersBtn = () => null; + const ManageServersRow = () => null; + const useStateFlagTimeout = jest.fn().mockReturnValue([ false, jest.fn() ]); + const ManageServers = createManageServers(serversExporter, ImportServersBtn, useStateFlagTimeout, ManageServersRow); + let wrapper: ShallowWrapper; + const createServerMock = (value: string, autoConnect = false) => Mock.of( + { id: value, name: value, url: value, autoConnect }, + ); + const createWrapper = (servers: ServersMap = {}) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); + + it('shows search field which allows searching servers, affecting te amount of rendered rows', () => { + const wrapper = createWrapper({ + foo: createServerMock('foo'), + bar: createServerMock('bar'), + baz: createServerMock('baz'), + }); + const searchBar = wrapper.find(SearchField); + + expect(wrapper.find(ManageServersRow)).toHaveLength(3); + expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + + searchBar.simulate('change', 'foo'); + expect(wrapper.find(ManageServersRow)).toHaveLength(1); + expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + + searchBar.simulate('change', 'ba'); + expect(wrapper.find(ManageServersRow)).toHaveLength(2); + expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + + searchBar.simulate('change', 'invalid'); + expect(wrapper.find(ManageServersRow)).toHaveLength(0); + expect(wrapper.find('tbody').find('tr')).toHaveLength(1); + }); + + it.each([ + [ createServerMock('foo'), 3 ], + [ createServerMock('foo', true), 4 ], + ])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { + const wrapper = createWrapper({ server }); + const row = wrapper.find(ManageServersRow); + + expect(wrapper.find('th')).toHaveLength(expectedCols); + expect(row.prop('hasAutoConnect')).toEqual(server.autoConnect); + }); + + it.each([ + [{}, 1 ], + [{ foo: createServerMock('foo') }, 2 ], + ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { + const wrapper = createWrapper(servers); + const exportBtn = wrapper.find(Button); + + expect(exportBtn).toHaveLength(expectedButtons); + }); + + it('allows exporting servers when clicking on button', () => { + const wrapper = createWrapper({ foo: createServerMock('foo') }); + const exportBtn = wrapper.find(Button).first(); + + expect(exportServers).not.toHaveBeenCalled(); + exportBtn.simulate('click'); + expect(exportServers).toHaveBeenCalled(); + }); + + it('shows an error message if an error occurs while importing servers', () => { + useStateFlagTimeout.mockReturnValue([ true, jest.fn() ]); + + const wrapper = createWrapper({ foo: createServerMock('foo') }); + const result = wrapper.find(Result); + + expect(result).toHaveLength(1); + expect(result.prop('type')).toEqual('error'); + }); +}); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx new file mode 100644 index 00000000..7a1a7658 --- /dev/null +++ b/test/servers/ManageServersRow.test.tsx @@ -0,0 +1,66 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { UncontrolledTooltip } from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Link } from 'react-router-dom'; +import { ManageServersRow as createManageServersRow } from '../../src/servers/ManageServersRow'; +import { ServerWithId } from '../../src/servers/data'; + +describe('', () => { + const ManageServersRowDropdown = () => null; + const ManageServersRow = createManageServersRow(ManageServersRowDropdown); + const server: ServerWithId = { + name: 'My server', + url: 'https://example.com', + apiKey: '123', + id: 'abc', + }; + let wrapper: ShallowWrapper; + const createWrapper = (hasAutoConnect = false, autoConnect = false) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + + it.each([ + [ true, 4 ], + [ false, 3 ], + ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { + const wrapper = createWrapper(hasAutoConnect); + const td = wrapper.find('td'); + const th = wrapper.find('th'); + + expect(td.length + th.length).toEqual(expectedCols); + }); + + it('renders a dropdown', () => { + const wrapper = createWrapper(); + const dropdown = wrapper.find(ManageServersRowDropdown); + + expect(dropdown).toHaveLength(1); + expect(dropdown.prop('server')).toEqual(expect.objectContaining(server)); + }); + + it.each([ + [ true, 1 ], + [ false, 0 ], + ])('renders auto-connect icon only if server is autoConnect', (autoConnect, expectedIcons) => { + const wrapper = createWrapper(true, autoConnect); + const icon = wrapper.find(FontAwesomeIcon); + const iconTooltip = wrapper.find(UncontrolledTooltip); + + expect(icon).toHaveLength(expectedIcons); + expect(iconTooltip).toHaveLength(expectedIcons); + }); + + it('renders server props where appropriate', () => { + const wrapper = createWrapper(); + const link = wrapper.find(Link); + const td = wrapper.find('td').first(); + + expect(link.prop('to')).toEqual(`/server/${server.id}`); + expect(link.prop('children')).toEqual(server.name); + expect(td.prop('children')).toEqual(server.url); + }); +}); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx new file mode 100644 index 00000000..e8dd609a --- /dev/null +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -0,0 +1,84 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import { DropdownItem } from 'reactstrap'; +import { ServerWithId } from '../../src/servers/data'; +import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; + +describe('', () => { + const DeleteServerModal = () => null; + const ManageServersRowDropdown = createManageServersRowDropdown(DeleteServerModal); + const setAutoConnect = jest.fn(); + let wrapper: ShallowWrapper; + const createWrapper = (autoConnect = false) => { + const server = Mock.of({ id: 'abc123', autoConnect }); + + wrapper = shallow(); + + return wrapper; + }; + + afterEach(jest.clearAllMocks); + + it('renders expected amount of dropdown items', () => { + const wrapper = createWrapper(); + const items = wrapper.find(DropdownItem); + + expect(items).toHaveLength(5); + expect(items.find('[divider]')).toHaveLength(1); + expect(items.at(0).prop('to')).toEqual('/server/abc123'); + expect(items.at(1).prop('to')).toEqual('/server/abc123/edit'); + }); + + it('allows toggling auto-connect', () => { + const wrapper = createWrapper(); + + expect(setAutoConnect).not.toHaveBeenCalled(); + wrapper.find(DropdownItem).at(2).simulate('click'); + expect(setAutoConnect).toHaveBeenCalledWith(expect.objectContaining({ id: 'abc123' }), true); + }); + + it('renders a modal', () => { + const wrapper = createWrapper(); + const modal = wrapper.find(DeleteServerModal); + + expect(modal).toHaveLength(1); + expect(modal.prop('redirectHome')).toEqual(false); + expect(modal.prop('server')).toEqual(expect.objectContaining({ id: 'abc123' })); + expect(modal.prop('isOpen')).toEqual(false); + }); + + it('allows toggling the modal', () => { + const wrapper = createWrapper(); + const modalToggle = wrapper.find(DropdownItem).last(); + + expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); + + modalToggle.simulate('click'); + expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(true); + + (wrapper.find(DeleteServerModal).prop('toggle') as Function)(); + expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); + }); + + it('can be toggled', () => { + const wrapper = createWrapper(); + + expect(wrapper.prop('isOpen')).toEqual(false); + + (wrapper.prop('toggle') as Function)(); + expect(wrapper.prop('isOpen')).toEqual(true); + + (wrapper.prop('toggle') as Function)(); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + it.each([ + [ true, 'Do not auto-connect' ], + [ false, 'Auto-connect' ], + ])('shows different auto-connect toggle text depending on current server status', (autoConnect, expectedText) => { + const wrapper = createWrapper(autoConnect); + const item = wrapper.find(DropdownItem).at(2); + + expect(item.html()).toContain(expectedText); + }); +}); diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index 2730979b..eab1eca2 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -1,15 +1,12 @@ import { values } from 'ramda'; import { Mock } from 'ts-mockery'; -import { FC } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { DropdownItem, DropdownToggle } from 'reactstrap'; -import serversDropdownCreator, { ServersDropdownProps } from '../../src/servers/ServersDropdown'; +import ServersDropdown from '../../src/servers/ServersDropdown'; import { ServerWithId } from '../../src/servers/data'; -import ServersExporter from '../../src/servers/services/ServersExporter'; describe('', () => { let wrapped: ShallowWrapper; - let ServersDropdown: FC; const servers = { '1a': Mock.of({ name: 'foo', id: '1a' }), '2b': Mock.of({ name: 'bar', id: '2b' }), @@ -17,13 +14,12 @@ describe('', () => { }; beforeEach(() => { - ServersDropdown = serversDropdownCreator(Mock.of()); wrapped = shallow(); }); afterEach(() => wrapped.unmount()); it('contains the list of servers, the divider, the create button and the export button', () => - expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 3)); + expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 2)); it('contains a toggle with proper title', () => expect(wrapped.find(DropdownToggle)).toHaveLength(1)); @@ -32,7 +28,7 @@ describe('', () => { const items = wrapped.find(DropdownItem); expect(items.filter('[divider]')).toHaveLength(1); - expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1); + expect(items.filterWhere((item) => item.prop('to') === '/manage-servers')).toHaveLength(1); }); it('shows only create link when no servers exist yet', () => { diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 261e6e72..fd8c41af 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { UncontrolledTooltip } from 'reactstrap'; import { Mock } from 'ts-mockery'; @@ -15,25 +16,59 @@ describe('', () => { const fileRef = { current: Mock.of({ click }), }; - - beforeEach(() => { - jest.clearAllMocks(); - - const ImportServersBtn = importServersBtnConstruct(serversImporterMock); - + const ImportServersBtn = importServersBtnConstruct(serversImporterMock); + const createWrapper = (className?: string, children?: ReactNode) => { wrapper = shallow( - , + , ); - }); + + return wrapper; + }; + + afterEach(jest.clearAllMocks); afterEach(() => wrapper.unmount()); it('renders a button, a tooltip and a file input', () => { + const wrapper = createWrapper(); + expect(wrapper.find('#importBtn')).toHaveLength(1); expect(wrapper.find(UncontrolledTooltip)).toHaveLength(1); - expect(wrapper.find('.create-server__csv-select')).toHaveLength(1); + expect(wrapper.find('.import-servers-btn__csv-select')).toHaveLength(1); + }); + + it.each([ + [ undefined, '' ], + [ 'foo', 'foo' ], + [ 'bar', 'bar' ], + ])('allows a class name to be provided', (providedClassName, expectedClassName) => { + const wrapper = createWrapper(providedClassName); + + expect(wrapper.find('#importBtn').prop('className')).toEqual(expectedClassName); + }); + + it.each([ + [ undefined, true ], + [ 'foo', false ], + [ 'bar', false ], + ])('has expected text', (children, expectToHaveDefaultText) => { + const wrapper = createWrapper(undefined, children); + + if (expectToHaveDefaultText) { + expect(wrapper.find('#importBtn').html()).toContain('Import from file'); + } else { + expect(wrapper.find('#importBtn').html()).toContain(children); + expect(wrapper.find('#importBtn').html()).not.toContain('Import from file'); + } }); it('triggers click on file ref when button is clicked', () => { + const wrapper = createWrapper(); const btn = wrapper.find('#importBtn'); btn.simulate('click'); @@ -42,7 +77,8 @@ describe('', () => { }); it('imports servers when file input changes', (done) => { - const file = wrapper.find('.create-server__csv-select'); + const wrapper = createWrapper(); + const file = wrapper.find('.import-servers-btn__csv-select'); file.simulate('change', { target: { files: [ '' ] } }); 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 }); + }); }); }); }); diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index cd718662..1f34137d 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -13,6 +13,6 @@ describe('', () => { expect(layout).toHaveLength(1); expect(sections).toHaveLength(1); - expect((sections.prop('items') as any[]).flat()).toHaveLength(4); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + expect((sections.prop('items') as any[]).flat()).toHaveLength(4); }); }); diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index cc0406ce..f7286649 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -77,7 +77,7 @@ describe('', () => { it('invokes order icon rendering', () => { const renderIcon = (field: OrderableFields) => - (wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: OrderableFields) => ReactElement | null)(field); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: OrderableFields) => ReactElement | null)(field); expect(renderIcon('visits')).toEqual(null); diff --git a/test/tags/TagsCards.test.tsx b/test/tags/TagsCards.test.tsx index 0ef3fed9..8b5d8db3 100644 --- a/test/tags/TagsCards.test.tsx +++ b/test/tags/TagsCards.test.tsx @@ -31,7 +31,7 @@ describe('', () => { const card = () => wrapper.find(TagCard).at(5); expect(card().prop('displayed')).toEqual(false); - (card().prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (card().prop('toggle') as Function)(); expect(card().prop('displayed')).toEqual(true); }); }); diff --git a/test/tags/TagsTableRow.test.tsx b/test/tags/TagsTableRow.test.tsx index b5ae6a2d..9787fbf9 100644 --- a/test/tags/TagsTableRow.test.tsx +++ b/test/tags/TagsTableRow.test.tsx @@ -68,11 +68,11 @@ describe('', () => { const wrapper = createWrapper(); expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(false); - (wrapper.find(EditTagModal).prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (wrapper.find(EditTagModal).prop('toggle') as Function)(); expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(true); expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(false); - (wrapper.find(DeleteTagConfirmModal).prop('toggle') as Function)(); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + (wrapper.find(DeleteTagConfirmModal).prop('toggle') as Function)(); expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(true); }); });