Fix some instances of react-hooks/exhaustive-deps

This commit is contained in:
Alejandro Celaya 2023-09-02 19:08:12 +02:00
parent 4298893960
commit fbc47846e3
6 changed files with 47 additions and 32 deletions

14
package-lock.json generated
View file

@ -15,7 +15,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@json2csv/plainjs": "^7.0.3", "@json2csv/plainjs": "^7.0.3",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@shlinkio/shlink-frontend-kit": "^0.2.0", "@shlinkio/shlink-frontend-kit": "^0.2.1",
"@shlinkio/shlink-js-sdk": "^0.1.0", "@shlinkio/shlink-js-sdk": "^0.1.0",
"@shlinkio/shlink-web-component": "^0.3.3", "@shlinkio/shlink-web-component": "^0.3.3",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
@ -2801,9 +2801,9 @@
} }
}, },
"node_modules/@shlinkio/shlink-frontend-kit": { "node_modules/@shlinkio/shlink-frontend-kit": {
"version": "0.2.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.2.1.tgz",
"integrity": "sha512-8kGaae0bTiGzbLzPsolLvJ5ud37BR2b1WeDy8lyXIiwoFiSAMIgWpqro0nMdBVBQXovjmMbtiS6BFYsaoBo9/g==", "integrity": "sha512-5mRpQII9bGHAJQ1ghgGY+jFC5tD0y0ufgPqco9vLVGXGVf+BSNQrTiw/Cx6f9eCInqFPgFo8CdJzWUHbIzvC+Q==",
"dependencies": { "dependencies": {
"classnames": "^2.3.2", "classnames": "^2.3.2",
"qs": "^6.11.2", "qs": "^6.11.2",
@ -12390,9 +12390,9 @@
} }
}, },
"@shlinkio/shlink-frontend-kit": { "@shlinkio/shlink-frontend-kit": {
"version": "0.2.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.2.1.tgz",
"integrity": "sha512-8kGaae0bTiGzbLzPsolLvJ5ud37BR2b1WeDy8lyXIiwoFiSAMIgWpqro0nMdBVBQXovjmMbtiS6BFYsaoBo9/g==", "integrity": "sha512-5mRpQII9bGHAJQ1ghgGY+jFC5tD0y0ufgPqco9vLVGXGVf+BSNQrTiw/Cx6f9eCInqFPgFo8CdJzWUHbIzvC+Q==",
"requires": { "requires": {
"classnames": "^2.3.2", "classnames": "^2.3.2",
"qs": "^6.11.2", "qs": "^6.11.2",

View file

@ -31,7 +31,7 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@json2csv/plainjs": "^7.0.3", "@json2csv/plainjs": "^7.0.3",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@shlinkio/shlink-frontend-kit": "^0.2.0", "@shlinkio/shlink-frontend-kit": "^0.2.1",
"@shlinkio/shlink-js-sdk": "^0.1.0", "@shlinkio/shlink-js-sdk": "^0.1.0",
"@shlinkio/shlink-web-component": "^0.3.3", "@shlinkio/shlink-web-component": "^0.3.3",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",

View file

@ -23,7 +23,7 @@ export const Home = ({ servers }: HomeProps) => {
// Try to redirect to the first server marked as auto-connect // Try to redirect to the first server marked as auto-connect
const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect); const autoConnectServer = serversList.find(({ autoConnect }) => autoConnect);
autoConnectServer && navigate(`/server/${autoConnectServer.id}`); autoConnectServer && navigate(`/server/${autoConnectServer.id}`);
}, []); }, [serversList, navigate]);
return ( return (
<div className="home"> <div className="home">

View file

@ -10,14 +10,15 @@ import { ShlinkLogo } from './img/ShlinkLogo';
import './MainHeader.scss'; import './MainHeader.scss';
export const MainHeader = (ServersDropdown: FC) => () => { export const MainHeader = (ServersDropdown: FC) => () => {
const [isOpen, toggleOpen, , close] = useToggle(); const [isNotCollapsed, toggleCollapse, , collapse] = useToggle();
const location = useLocation(); const location = useLocation();
const { pathname } = location; const { pathname } = location;
useEffect(close, [location]); // In mobile devices, collapse the navbar when location changes
useEffect(collapse, [location, collapse]);
const settingsPath = '/settings'; const settingsPath = '/settings';
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen }); const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isNotCollapsed });
return ( return (
<Navbar color="primary" dark fixed="top" className="main-header" expand="md"> <Navbar color="primary" dark fixed="top" className="main-header" expand="md">
@ -25,11 +26,11 @@ export const MainHeader = (ServersDropdown: FC) => () => {
<ShlinkLogo className="main-header__brand-logo" color="white" /> Shlink <ShlinkLogo className="main-header__brand-logo" color="white" /> Shlink
</NavbarBrand> </NavbarBrand>
<NavbarToggler onClick={toggleOpen}> <NavbarToggler onClick={toggleCollapse}>
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} /> <FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
</NavbarToggler> </NavbarToggler>
<Collapse navbar isOpen={isOpen}> <Collapse navbar isOpen={isNotCollapsed}>
<Nav navbar className="ms-auto"> <Nav navbar className="ms-auto">
<NavItem> <NavItem>
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}> <NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>

View file

@ -1,6 +1,6 @@
import { Result, useToggle } from '@shlinkio/shlink-frontend-kit'; import { Result, useToggle } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react'; import type { FC } from 'react';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -37,25 +37,25 @@ export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTim
const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [isConfirmModalOpen, toggleConfirmModal] = useToggle(); const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
const [serverData, setServerData] = useState<ServerData | undefined>(); const [serverData, setServerData] = useState<ServerData>();
const save = () => { const saveNewServer = useCallback((theServerData: ServerData) => {
const id = uuid();
createServers([{ ...theServerData, id }]);
navigate(`/server/${id}`);
}, [createServers, navigate]);
useEffect(() => {
if (!serverData) { if (!serverData) {
return; return;
} }
const id = uuid();
createServers([{ ...serverData, id }]);
navigate(`/server/${id}`);
};
useEffect(() => {
const serverExists = Object.values(servers).some( const serverExists = Object.values(servers).some(
({ url, apiKey }) => serverData?.url === url && serverData?.apiKey === apiKey, ({ url, apiKey }) => serverData?.url === url && serverData?.apiKey === apiKey,
); );
serverExists ? toggleConfirmModal() : save(); serverExists ? toggleConfirmModal() : saveNewServer(serverData);
}, [serverData]); }, [saveNewServer, serverData, servers, toggleConfirmModal]);
return ( return (
<NoMenuLayout> <NoMenuLayout>
@ -74,7 +74,7 @@ export const CreateServer = (ImportServersBtn: FC<ImportServersBtnProps>, useTim
isOpen={isConfirmModalOpen} isOpen={isConfirmModalOpen}
duplicatedServers={serverData ? [serverData] : []} duplicatedServers={serverData ? [serverData] : []}
onDiscard={goBack} onDiscard={goBack}
onSave={save} onSave={() => serverData && saveNewServer(serverData)}
/> />
</NoMenuLayout> </NoMenuLayout>
); );

View file

@ -2,7 +2,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { CreateServer as createCreateServer } from '../../src/servers/CreateServer'; import { CreateServer as createCreateServer } from '../../src/servers/CreateServer';
import type { ServerWithId } from '../../src/servers/data'; import type { ServersMap } from '../../src/servers/data';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
vi.mock('react-router-dom', async () => ({ vi.mock('react-router-dom', async () => ({
@ -10,11 +10,19 @@ vi.mock('react-router-dom', async () => ({
useNavigate: vi.fn(), useNavigate: vi.fn(),
})); }));
type SetUpOptions = {
serversImported?: boolean;
importFailed?: boolean;
servers?: ServersMap;
};
describe('<CreateServer />', () => { describe('<CreateServer />', () => {
const createServersMock = vi.fn(); const createServersMock = vi.fn();
const navigate = vi.fn(); const navigate = vi.fn();
const servers = { foo: fromPartial<ServerWithId>({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }) }; const defaultServers: ServersMap = {
const setUp = (serversImported = false, importFailed = false) => { foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }),
};
const setUp = ({ serversImported = false, importFailed = false, servers = defaultServers }: SetUpOptions = {}) => {
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
let callCount = 0; let callCount = 0;
@ -29,16 +37,17 @@ describe('<CreateServer />', () => {
}; };
it('shows success message when imported is true', () => { it('shows success message when imported is true', () => {
setUp(true); setUp({ serversImported: true });
expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument();
expect( expect(
screen.queryByText('The servers could not be imported. Make sure the format is correct.'), screen.queryByText('The servers could not be imported. Make sure the format is correct.'),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.queryByText('ImportServersBtn')).not.toBeInTheDocument();
}); });
it('shows error message when import failed', () => { it('shows error message when import failed', () => {
setUp(false, true); setUp({ importFailed: true });
expect( expect(
screen.queryByText('Servers properly imported. You can now select one from the list :)'), screen.queryByText('Servers properly imported. You can now select one from the list :)'),
@ -46,6 +55,11 @@ describe('<CreateServer />', () => {
expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument(); expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument();
}); });
it('shows import button when no servers exist yet', () => {
setUp({ servers: {} });
expect(screen.queryByText('ImportServersBtn')).toBeInTheDocument();
});
it('creates server data when form is submitted', async () => { it('creates server data when form is submitted', async () => {
const { user } = setUp(); const { user } = setUp();