From f07e7fd31ccde0a60af879d2a69733a7fb258efb Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Dec 2021 10:02:31 +0100 Subject: [PATCH 1/7] Simplified server-related styles and removed default export from NoMenuLayout --- src/common/NoMenuLayout.tsx | 4 +--- src/servers/CreateServer.scss | 10 ---------- src/servers/CreateServer.tsx | 3 +-- src/servers/EditServer.tsx | 2 +- src/servers/ManageServers.tsx | 2 +- src/servers/helpers/ServerError.tsx | 2 +- src/servers/helpers/ServerForm.scss | 7 +++++++ src/servers/helpers/ServerForm.tsx | 2 +- src/servers/helpers/withSelectedServer.tsx | 2 +- src/settings/Settings.tsx | 2 +- test/common/MenuLayout.test.tsx | 2 +- test/settings/Settings.test.tsx | 2 +- 12 files changed, 17 insertions(+), 23 deletions(-) delete mode 100644 src/servers/CreateServer.scss diff --git a/src/common/NoMenuLayout.tsx b/src/common/NoMenuLayout.tsx index dfddde05..ea3862f2 100644 --- a/src/common/NoMenuLayout.tsx +++ b/src/common/NoMenuLayout.tsx @@ -1,6 +1,4 @@ import { FC } from 'react'; import './NoMenuLayout.scss'; -const NoMenuLayout: FC = ({ children }) =>
{children}
; - -export default NoMenuLayout; +export const NoMenuLayout: FC = ({ children }) =>
{children}
; diff --git a/src/servers/CreateServer.scss b/src/servers/CreateServer.scss deleted file mode 100644 index 4861c9af..00000000 --- a/src/servers/CreateServer.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import '../utils/base'; - -.create-server__label { - font-weight: 700; - cursor: pointer; - - @media (min-width: $mdMin) { - text-align: right; - } -} diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index e24e548c..b0e65f09 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -3,12 +3,11 @@ 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 { NoMenuLayout } from '../common/NoMenuLayout'; import { StateFlagTimeout } from '../utils/helpers/hooks'; import { ServerForm } from './helpers/ServerForm'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerData, ServersMap, ServerWithId } from './data'; -import './CreateServer.scss'; const SHOW_IMPORT_MSG_TIME = 4000; diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx index f6576066..514ebcba 100644 --- a/src/servers/EditServer.tsx +++ b/src/servers/EditServer.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { Button } from 'reactstrap'; -import NoMenuLayout from '../common/NoMenuLayout'; +import { NoMenuLayout } from '../common/NoMenuLayout'; import { ServerForm } from './helpers/ServerForm'; import { withSelectedServer } from './helpers/withSelectedServer'; import { isServerWithId, ServerData } from './data'; diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 9554f2e8..532d1329 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -3,7 +3,7 @@ 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 { NoMenuLayout } from '../common/NoMenuLayout'; import { SimpleCard } from '../utils/SimpleCard'; import SearchField from '../utils/SearchField'; import { Result } from '../utils/Result'; diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index 504bb610..c84a5708 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -4,7 +4,7 @@ import Message from '../../utils/Message'; import ServersListGroup from '../ServersListGroup'; import { DeleteServerButtonProps } from '../DeleteServerButton'; import { isServerWithId, SelectedServer, ServersMap } from '../data'; -import NoMenuLayout from '../../common/NoMenuLayout'; +import { NoMenuLayout } from '../../common/NoMenuLayout'; import './ServerError.scss'; interface ServerErrorProps { diff --git a/src/servers/helpers/ServerForm.scss b/src/servers/helpers/ServerForm.scss index 97b1ae22..0305ca8b 100644 --- a/src/servers/helpers/ServerForm.scss +++ b/src/servers/helpers/ServerForm.scss @@ -1,3 +1,10 @@ +@import '../../utils/base'; + .server-form .form-group:last-child { margin-bottom: 0; } + +.server-form__label { + font-weight: 700; + cursor: pointer; +} diff --git a/src/servers/helpers/ServerForm.tsx b/src/servers/helpers/ServerForm.tsx index 9370cbb3..ee6c8fff 100644 --- a/src/servers/helpers/ServerForm.tsx +++ b/src/servers/helpers/ServerForm.tsx @@ -12,7 +12,7 @@ interface ServerFormProps { } const FormGroup: FC = (props) => - ; + ; export const ServerForm: FC = ({ onSubmit, initialValues, children, title }) => { const [ name, setName ] = useState(''); diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 2c91d344..4daa74fc 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -2,7 +2,7 @@ import { FC, useEffect } from 'react'; import { RouteComponentProps } from 'react-router'; import Message from '../../utils/Message'; import { isNotFoundServer, SelectedServer } from '../data'; -import NoMenuLayout from '../../common/NoMenuLayout'; +import { NoMenuLayout } from '../../common/NoMenuLayout'; interface WithSelectedServerProps extends RouteComponentProps<{ serverId: string }> { selectServer: (serverId: string) => void; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 81d047ed..c3fce833 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,6 +1,6 @@ import { FC, ReactNode } from 'react'; import { Row } from 'reactstrap'; -import NoMenuLayout from '../common/NoMenuLayout'; +import { NoMenuLayout } from '../common/NoMenuLayout'; const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( <> diff --git a/test/common/MenuLayout.test.tsx b/test/common/MenuLayout.test.tsx index e45dfb3d..4ac16296 100644 --- a/test/common/MenuLayout.test.tsx +++ b/test/common/MenuLayout.test.tsx @@ -5,7 +5,7 @@ import { Route } from 'react-router-dom'; import { Mock } from 'ts-mockery'; import createMenuLayout from '../../src/common/MenuLayout'; import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data'; -import NoMenuLayout from '../../src/common/NoMenuLayout'; +import { NoMenuLayout } from '../../src/common/NoMenuLayout'; import { SemVer } from '../../src/utils/helpers/version'; describe('', () => { diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index 0810cf8f..ffd9ed39 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -1,6 +1,6 @@ import { shallow } from 'enzyme'; import createSettings from '../../src/settings/Settings'; -import NoMenuLayout from '../../src/common/NoMenuLayout'; +import { NoMenuLayout } from '../../src/common/NoMenuLayout'; describe('', () => { const Component = () => null; From 1f9356cc21422ad605c5d1ea81923321da10b0b9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Dec 2021 21:21:30 +0100 Subject: [PATCH 2/7] Created modal to warn when creating a duplicated server --- src/servers/CreateServer.tsx | 31 +++++++++++++++--- src/servers/helpers/DuplicatedServerModal.tsx | 31 ++++++++++++++++++ test/servers/CreateServer.test.tsx | 32 +++++++++++++++---- 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 src/servers/helpers/DuplicatedServerModal.tsx diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index b0e65f09..6eb3c2da 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,13 +1,14 @@ -import { FC } from 'react'; +import { FC, useEffect, useState } 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 { StateFlagTimeout, useToggle } from '../utils/helpers/hooks'; import { ServerForm } from './helpers/ServerForm'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerData, ServersMap, ServerWithId } from './data'; +import { DuplicatedServerModal } from './helpers/DuplicatedServerModal'; const SHOW_IMPORT_MSG_TIME = 4000; @@ -31,16 +32,30 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT 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) => { + const [ isConfirmModalOpen, toggleConfirmModal ] = useToggle(); + const [ serverData, setServerData ] = useState(); + const save = () => { + if (!serverData) { + return; + } + const id = uuid(); createServer({ ...serverData, id }); push(`/server/${id}`); }; + useEffect(() => { + const serverExists = Object.values(servers).some( + ({ url, apiKey }) => serverData?.url === url && serverData?.apiKey === apiKey, + ); + + serverExists ? toggleConfirmModal() : save(); + }, [ serverData ]); + return ( - Add new server} onSubmit={handleSubmit}> + Add new server} onSubmit={setServerData}> {!hasServers && } {hasServers && } @@ -49,6 +64,14 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT {serversImported && } {errorImporting && } + + ); }; diff --git a/src/servers/helpers/DuplicatedServerModal.tsx b/src/servers/helpers/DuplicatedServerModal.tsx new file mode 100644 index 00000000..497174c2 --- /dev/null +++ b/src/servers/helpers/DuplicatedServerModal.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { ServerData } from '../data'; + +interface DuplicatedServerModalProps { + serverData?: ServerData; + isOpen: boolean; + toggle: () => void; + onDiscard: () => void; + onSave: () => void; +} + +export const DuplicatedServerModal: FC = ( + { isOpen, toggle, serverData, onDiscard, onSave }, +) => ( + + Duplicated server + +

There is already a server with:

+
    +
  • URL: {serverData?.url}
  • +
  • API key: {serverData?.apiKey}
  • +
+ Do you want to save this server anyway? +
+ + + + +
+); diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index 7cb2e388..795db3e4 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -4,18 +4,21 @@ import { History } from 'history'; import createServerConstruct from '../../src/servers/CreateServer'; import { ServerForm } from '../../src/servers/helpers/ServerForm'; import { ServerWithId } from '../../src/servers/data'; +import { DuplicatedServerModal } from '../../src/servers/helpers/DuplicatedServerModal'; describe('', () => { let wrapper: ShallowWrapper; const ImportServersBtn = () => null; const createServerMock = jest.fn(); const push = jest.fn(); - const historyMock = Mock.of({ push }); + const goBack = jest.fn(); + const historyMock = Mock.of({ push, goBack }); const servers = { foo: Mock.all() }; const createWrapper = (serversImported = false, importFailed = false) => { const useStateFlagTimeout = jest.fn() .mockReturnValueOnce([ serversImported, () => '' ]) - .mockReturnValueOnce([ importFailed, () => '' ]); + .mockReturnValueOnce([ importFailed, () => '' ]) + .mockReturnValue([]); const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout); wrapper = shallow(); @@ -23,10 +26,8 @@ describe('', () => { return wrapper; }; - afterEach(() => { - jest.resetAllMocks(); - wrapper?.unmount(); - }); + beforeEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); it('renders components', () => { const wrapper = createWrapper(); @@ -51,13 +52,30 @@ describe('', () => { expect(result.prop('type')).toEqual('error'); }); - it('creates server and redirects to it when form is submitted', () => { + it('creates server data form is submitted', () => { const wrapper = createWrapper(); const form = wrapper.find(ServerForm); + expect(wrapper.find(DuplicatedServerModal).prop('serverData')).not.toBeDefined(); form.simulate('submit', {}); + expect(wrapper.find(DuplicatedServerModal).prop('serverData')).toEqual({}); + }); + + it('saves server and redirects on modal save', () => { + const wrapper = createWrapper(); + + wrapper.find(ServerForm).simulate('submit', {}); + wrapper.find(DuplicatedServerModal).simulate('save'); expect(createServerMock).toHaveBeenCalledTimes(1); expect(push).toHaveBeenCalledTimes(1); }); + + it('goes back on modal discard', () => { + const wrapper = createWrapper(); + + wrapper.find(DuplicatedServerModal).simulate('discard'); + + expect(goBack).toHaveBeenCalledTimes(1); + }); }); From 053b38bee3dafb506748a4cd0f908574d5b467af Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 30 Dec 2021 21:40:08 +0100 Subject: [PATCH 3/7] Created DuplicatedServerModal test --- .../helpers/DuplicatedServerModal.test.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/servers/helpers/DuplicatedServerModal.test.tsx diff --git a/test/servers/helpers/DuplicatedServerModal.test.tsx b/test/servers/helpers/DuplicatedServerModal.test.tsx new file mode 100644 index 00000000..4eaa5737 --- /dev/null +++ b/test/servers/helpers/DuplicatedServerModal.test.tsx @@ -0,0 +1,56 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import { Button } from 'reactstrap'; +import { DuplicatedServerModal } from '../../../src/servers/helpers/DuplicatedServerModal'; +import { ServerData } from '../../../src/servers/data'; + +describe('', () => { + const onDiscard = jest.fn(); + const onSave = jest.fn(); + let wrapper: ShallowWrapper; + const createWrapper = (serverData?: ServerData) => { + wrapper = shallow( + , + ); + + return wrapper; + }; + + beforeEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); + + it.each([ + [ undefined ], + [ Mock.of({ url: 'url', apiKey: 'apiKey' }) ], + ])('displays provided server data', (serverData) => { + const wrapper = createWrapper(serverData); + const li = wrapper.find('li'); + + expect(li.first().find('b').html()).toEqual(`${serverData?.url ?? ''}`); + expect(li.last().find('b').html()).toEqual(`${serverData?.apiKey ?? ''}`); + }); + + it('invokes onDiscard when appropriate button is clicked', () => { + const wrapper = createWrapper(); + const btn = wrapper.find(Button).first(); + + btn.simulate('click'); + + expect(onDiscard).toHaveBeenCalled(); + }); + + it('invokes onSave when appropriate button is clicked', () => { + const wrapper = createWrapper(); + const btn = wrapper.find(Button).last(); + + btn.simulate('click'); + + expect(onSave).toHaveBeenCalled(); + }); +}); From 3cb066f5f581fd97f65cc2db9acd332344a5da2f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 31 Dec 2021 17:56:37 +0100 Subject: [PATCH 4/7] Reduced unnecesary lines in test --- test/servers/helpers/DuplicatedServerModal.test.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/servers/helpers/DuplicatedServerModal.test.tsx b/test/servers/helpers/DuplicatedServerModal.test.tsx index 4eaa5737..8e3124a3 100644 --- a/test/servers/helpers/DuplicatedServerModal.test.tsx +++ b/test/servers/helpers/DuplicatedServerModal.test.tsx @@ -10,13 +10,7 @@ describe('', () => { let wrapper: ShallowWrapper; const createWrapper = (serverData?: ServerData) => { wrapper = shallow( - , + , ); return wrapper; From 98398a048bdae49481f3251d24aa85c38889ede2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 1 Jan 2022 12:20:09 +0100 Subject: [PATCH 5/7] Added logic to detect duplicated servers when importing a servers list --- src/servers/CreateServer.tsx | 7 ++- src/servers/helpers/DuplicatedServerModal.tsx | 31 ------------ .../helpers/DuplicatedServersModal.tsx | 38 ++++++++++++++ src/servers/helpers/ImportServersBtn.tsx | 50 ++++++++++++++++--- src/servers/services/ServersImporter.ts | 2 +- src/servers/services/provideServices.ts | 4 +- test/servers/CreateServer.test.tsx | 12 ++--- ...st.tsx => DuplicatedServersModal.test.tsx} | 24 +++++---- .../servers/helpers/ImportServersBtn.test.tsx | 31 ++++++++---- test/servers/services/ServersImporter.test.ts | 2 +- 10 files changed, 128 insertions(+), 73 deletions(-) delete mode 100644 src/servers/helpers/DuplicatedServerModal.tsx create mode 100644 src/servers/helpers/DuplicatedServersModal.tsx rename test/servers/helpers/{DuplicatedServerModal.test.tsx => DuplicatedServersModal.test.tsx} (53%) diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 6eb3c2da..0412016a 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -8,7 +8,7 @@ import { StateFlagTimeout, useToggle } from '../utils/helpers/hooks'; import { ServerForm } from './helpers/ServerForm'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerData, ServersMap, ServerWithId } from './data'; -import { DuplicatedServerModal } from './helpers/DuplicatedServerModal'; +import { DuplicatedServersModal } from './helpers/DuplicatedServersModal'; const SHOW_IMPORT_MSG_TIME = 4000; @@ -65,10 +65,9 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT {serversImported && } {errorImporting && } - diff --git a/src/servers/helpers/DuplicatedServerModal.tsx b/src/servers/helpers/DuplicatedServerModal.tsx deleted file mode 100644 index 497174c2..00000000 --- a/src/servers/helpers/DuplicatedServerModal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { FC } from 'react'; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import { ServerData } from '../data'; - -interface DuplicatedServerModalProps { - serverData?: ServerData; - isOpen: boolean; - toggle: () => void; - onDiscard: () => void; - onSave: () => void; -} - -export const DuplicatedServerModal: FC = ( - { isOpen, toggle, serverData, onDiscard, onSave }, -) => ( - - Duplicated server - -

There is already a server with:

-
    -
  • URL: {serverData?.url}
  • -
  • API key: {serverData?.apiKey}
  • -
- Do you want to save this server anyway? -
- - - - -
-); diff --git a/src/servers/helpers/DuplicatedServersModal.tsx b/src/servers/helpers/DuplicatedServersModal.tsx new file mode 100644 index 00000000..238d2f10 --- /dev/null +++ b/src/servers/helpers/DuplicatedServersModal.tsx @@ -0,0 +1,38 @@ +import { FC, Fragment } from 'react'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { ServerData } from '../data'; + +interface DuplicatedServersModalProps { + duplicatedServers: ServerData[]; + isOpen: boolean; + onDiscard: () => void; + onSave: () => void; +} + +export const DuplicatedServersModal: FC = ( + { isOpen, duplicatedServers, onDiscard, onSave }, +) => { + const hasMultipleServers = duplicatedServers.length > 1; + + return ( + + Duplicated server{hasMultipleServers && 's'} + +

{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}

+
    + {duplicatedServers.map(({ url, apiKey }, index) => !hasMultipleServers ? ( + +
  • URL: {url}
  • +
  • API key: {apiKey}
  • +
    + ) :
  • {url} - {apiKey}
  • )} +
+ {hasMultipleServers ? 'Do you want to ignore duplicated servers?' : 'Do you want to save this server anyway?'} +
+ + + + +
+ ); +}; diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 3f27cfc2..2a91e71c 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -1,10 +1,12 @@ -import { useRef, RefObject, ChangeEvent, MutableRefObject, FC } from 'react'; +import { useRef, RefObject, ChangeEvent, MutableRefObject, FC, useState, useEffect } from 'react'; import { Button, UncontrolledTooltip } from 'reactstrap'; -import { pipe } from 'ramda'; +import { complement, pipe } from 'ramda'; 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 { useToggle } from '../../utils/helpers/hooks'; +import { ServersImporter } from '../services/ServersImporter'; +import { ServerData, ServersMap } from '../data'; +import { DuplicatedServersModal } from './DuplicatedServersModal'; import './ImportServersBtn.scss'; type Ref = RefObject | MutableRefObject; @@ -18,11 +20,16 @@ export interface ImportServersBtnProps { interface ImportServersBtnConnectProps extends ImportServersBtnProps { createServers: (servers: ServerData[]) => void; + servers: ServersMap; fileRef: Ref; } +const serversFiltering = (servers: ServerData[]) => + ({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey); + const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC => ({ createServers, + servers, fileRef, children, onImport = () => {}, @@ -31,15 +38,37 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC { const ref = fileRef ?? useRef(); - const onChange = async ({ target }: ChangeEvent) => + const [ serversToCreate, setServersToCreate ] = useState(); + const [ duplicatedServers, setDuplicatedServers ] = useState([]); + const [ isModalOpen,, showModal, hideModal ] = useToggle(); + const create = pipe(createServers, onImport); + const createAllServers = pipe(() => create(serversToCreate ?? []), hideModal); + const createNonDuplicatedServers = pipe( + () => create((serversToCreate ?? []).filter(complement(serversFiltering(duplicatedServers)))), + hideModal, + ); + const onFile = async ({ target }: ChangeEvent) => importServersFromFile(target.files?.[0]) - .then(pipe(createServers, onImport)) + .then(setServersToCreate) .then(() => { // Reset input after processing file (target as { value: string | null }).value = null; }) .catch(onImportError); + useEffect(() => { + if (!serversToCreate) { + return; + } + + const existingServers = Object.values(servers); + const duplicatedServers = serversToCreate.filter(serversFiltering(existingServers)); + const hasDuplicatedServers = !!duplicatedServers.length; + + !hasDuplicatedServers ? create(serversToCreate) : setDuplicatedServers(duplicatedServers); + hasDuplicatedServers && showModal(); + }, [ serversToCreate ]); + return ( <> diff --git a/test/servers/helpers/DuplicatedServersModal.test.tsx b/test/servers/helpers/DuplicatedServersModal.test.tsx index 8810aac3..6e2b6731 100644 --- a/test/servers/helpers/DuplicatedServersModal.test.tsx +++ b/test/servers/helpers/DuplicatedServersModal.test.tsx @@ -1,6 +1,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; -import { Button } from 'reactstrap'; +import { Button, ModalHeader } from 'reactstrap'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { ServerData } from '../../../src/servers/data'; @@ -19,6 +19,51 @@ describe('', () => { beforeEach(jest.clearAllMocks); afterEach(() => wrapper?.unmount()); + it.each([ + [[], 0 ], + [[ Mock.all() ], 2 ], + [[ Mock.all(), Mock.all() ], 2 ], + [[ Mock.all(), Mock.all(), Mock.all() ], 3 ], + [[ Mock.all(), Mock.all(), Mock.all(), Mock.all() ], 4 ], + ])('renders expected amount of items', (duplicatedServers, expectedItems) => { + const wrapper = createWrapper(duplicatedServers); + const li = wrapper.find('li'); + + expect(li).toHaveLength(expectedItems); + }); + + it.each([ + [ + [ Mock.all() ], + { + header: 'Duplicated server', + firstParagraph: 'There is already a server with:', + lastParagraph: 'Do you want to save this server anyway?', + discardBtn: 'Discard', + }, + ], + [ + [ Mock.all(), Mock.all() ], + { + header: 'Duplicated servers', + firstParagraph: 'The next servers already exist:', + lastParagraph: 'Do you want to ignore duplicated servers?', + discardBtn: 'Ignore duplicated', + }, + ], + ])('renders expected texts based on amount of servers', (duplicatedServers, assertions) => { + const wrapper = createWrapper(duplicatedServers); + const header = wrapper.find(ModalHeader); + const p = wrapper.find('p'); + const span = wrapper.find('span'); + const discardBtn = wrapper.find(Button).first(); + + expect(header.html()).toContain(assertions.header); + expect(p.html()).toContain(assertions.firstParagraph); + expect(span.html()).toContain(assertions.lastParagraph); + expect(discardBtn.html()).toContain(assertions.discardBtn); + }); + it.each([ [[]], [[ Mock.of({ url: 'url', apiKey: 'apiKey' }) ]], @@ -28,9 +73,16 @@ describe('', () => { if (duplicatedServers.length === 0) { expect(li).toHaveLength(0); - } else { + } else if (duplicatedServers.length === 1) { expect(li.first().find('b').html()).toEqual(`${duplicatedServers[0].url}`); expect(li.last().find('b').html()).toEqual(`${duplicatedServers[0].apiKey}`); + } else { + expect.assertions(duplicatedServers.length); + li.forEach((item, index) => { + const server = duplicatedServers[index]; + + expect(item.html()).toContain(`${server.url} - ${server.apiKey}`); + }); } }); From ba667a0768015178e5318f76b2bf1e2091a685d8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 1 Jan 2022 12:38:00 +0100 Subject: [PATCH 7/7] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534d1d80..85189cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [3.5.0] - 2022-01-01 ### Added * [#407](https://github.com/shlinkio/shlink-web-client/pull/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter". @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The warning includes a link to the documentation, explaining what are the steps to get it fixed. +* [#506](https://github.com/shlinkio/shlink-web-client/pull/506) Improved how servers are handled, dispaying a warning when creating or importing servers that already exist. * [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. * [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page. * [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.