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.
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..0412016a 100644
--- a/src/servers/CreateServer.tsx
+++ b/src/servers/CreateServer.tsx
@@ -1,14 +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 { NoMenuLayout } from '../common/NoMenuLayout';
+import { StateFlagTimeout, useToggle } from '../utils/helpers/hooks';
import { ServerForm } from './helpers/ServerForm';
import { ImportServersBtnProps } from './helpers/ImportServersBtn';
import { ServerData, ServersMap, ServerWithId } from './data';
-import './CreateServer.scss';
+import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
const SHOW_IMPORT_MSG_TIME = 4000;
@@ -32,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 && }
@@ -50,6 +64,13 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT
{serversImported && }
{errorImporting && }
+
+
);
};
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/DuplicatedServersModal.tsx b/src/servers/helpers/DuplicatedServersModal.tsx
new file mode 100644
index 00000000..b2fb3d78
--- /dev/null
+++ b/src/servers/helpers/DuplicatedServersModal.tsx
@@ -0,0 +1,40 @@
+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:'}