mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-02 22:01:52 +00:00
Extract logic to determine if a list of servers contains duplicates
This commit is contained in:
@@ -9,6 +9,7 @@ import { componentFactory, useDependencies } from '../../container/utils';
|
||||
import type { ServerData, ServersMap } from '../data';
|
||||
import type { ServersImporter } from '../services/ServersImporter';
|
||||
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
||||
import { dedupServers } from './index';
|
||||
|
||||
export type ImportServersBtnProps = PropsWithChildren<{
|
||||
onImport?: () => void;
|
||||
@@ -26,9 +27,6 @@ type ImportServersBtnDeps = {
|
||||
ServersImporter: ServersImporter
|
||||
};
|
||||
|
||||
const serversInclude = (servers: ServerData[], { url, apiKey }: ServerData) =>
|
||||
servers.some((server) => server.url === url && server.apiKey === apiKey);
|
||||
|
||||
const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBtnDeps> = ({
|
||||
createServers,
|
||||
servers,
|
||||
@@ -43,7 +41,9 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||
|
||||
const serversToCreate = useRef<ServerData[]>([]);
|
||||
const importedServersRef = useRef<ServerData[]>([]);
|
||||
const newServersRef = useRef<ServerData[]>([]);
|
||||
|
||||
const create = useCallback((serversData: ServerData[]) => {
|
||||
createServers(serversData);
|
||||
onImport();
|
||||
@@ -51,22 +51,21 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||
const onFile = useCallback(
|
||||
async ({ target }: ChangeEvent<HTMLInputElement>) =>
|
||||
serversImporter.importServersFromFile(target.files?.[0])
|
||||
.then((newServers) => {
|
||||
serversToCreate.current = newServers;
|
||||
.then((importedServers) => {
|
||||
const { duplicatedServers, newServers } = dedupServers(servers, importedServers);
|
||||
|
||||
const existingServers = Object.values(servers);
|
||||
const dupServers = newServers.filter((server) => serversInclude(existingServers, server));
|
||||
const hasDuplicatedServers = !!dupServers.length;
|
||||
importedServersRef.current = importedServers;
|
||||
newServersRef.current = newServers;
|
||||
|
||||
if (!hasDuplicatedServers) {
|
||||
create(newServers);
|
||||
if (duplicatedServers.length === 0) {
|
||||
create(importedServers);
|
||||
} else {
|
||||
setDuplicatedServers(dupServers);
|
||||
setDuplicatedServers(duplicatedServers);
|
||||
showModal();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Reset input after processing file
|
||||
// Reset file input after processing file
|
||||
(target as { value: string | null }).value = null;
|
||||
})
|
||||
.catch(onImportError),
|
||||
@@ -74,13 +73,13 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||
);
|
||||
|
||||
const createAllServers = useCallback(() => {
|
||||
create(serversToCreate.current);
|
||||
create(importedServersRef.current);
|
||||
hideModal();
|
||||
}, [create, hideModal, serversToCreate]);
|
||||
}, [create, hideModal]);
|
||||
const createNonDuplicatedServers = useCallback(() => {
|
||||
create(serversToCreate.current.filter((server) => !serversInclude(duplicatedServers, server)));
|
||||
create(newServersRef.current);
|
||||
hideModal();
|
||||
}, [create, duplicatedServers, hideModal]);
|
||||
}, [create, hideModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -91,7 +90,15 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||
You can create servers by importing a CSV file with <b>name</b>, <b>apiKey</b> and <b>url</b> columns.
|
||||
</UncontrolledTooltip>
|
||||
|
||||
<input type="file" accept=".csv" className="d-none" ref={ref} onChange={onFile} aria-hidden />
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv"
|
||||
className="d-none"
|
||||
aria-hidden
|
||||
ref={ref}
|
||||
onChange={onFile}
|
||||
data-testid="csv-file-input"
|
||||
/>
|
||||
|
||||
<DuplicatedServersModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
50
src/servers/helpers/index.ts
Normal file
50
src/servers/helpers/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { groupBy } from '@shlinkio/data-manipulation';
|
||||
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||
|
||||
/**
|
||||
* Builds a potentially unique ID for a server, based on concatenating their name and the hostname of their domain, all
|
||||
* in lowercase and replacing invalid URL characters with hyphens.
|
||||
*/
|
||||
function idForServer(server: ServerData): string {
|
||||
const url = new URL(server.url);
|
||||
return `${server.name} ${url.host}`.toLowerCase().replace(/[^a-zA-Z0-9-_.~]/g, '-');
|
||||
}
|
||||
|
||||
export function serverWithId(server: ServerWithId | ServerData): ServerWithId {
|
||||
if ('id' in server) {
|
||||
return server;
|
||||
}
|
||||
|
||||
const id = idForServer(server);
|
||||
return { ...server, id };
|
||||
}
|
||||
|
||||
export function serversListToMap(servers: ServerWithId[]): ServersMap {
|
||||
return servers.reduce<ServersMap>(
|
||||
(acc, server) => ({ ...acc, [server.id]: server }),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
const serversInclude = (serversList: ServerData[], { url, apiKey }: ServerData) =>
|
||||
serversList.some((server) => server.url === url && server.apiKey === apiKey);
|
||||
|
||||
export type DedupServersResult = {
|
||||
/** Servers which already exist in the reference list */
|
||||
duplicatedServers: ServerData[];
|
||||
/** Servers which are new based on a reference list */
|
||||
newServers: ServerData[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a list of new servers, checks which of them already exist in a servers map, and which don't
|
||||
*/
|
||||
export function dedupServers(servers: ServersMap, serversToAdd: ServerData[]): DedupServersResult {
|
||||
const serversList = Object.values(servers);
|
||||
const { duplicatedServers = [], newServers = [] } = groupBy(
|
||||
serversToAdd,
|
||||
(server) => serversInclude(serversList, server) ? 'duplicatedServers' : 'newServers',
|
||||
);
|
||||
|
||||
return { duplicatedServers, newServers };
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { randomUUID } from '../../utils/utils';
|
||||
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||
import { serversListToMap, serverWithId } from '../helpers';
|
||||
|
||||
interface EditServer {
|
||||
serverId: string;
|
||||
@@ -15,19 +15,6 @@ interface SetAutoConnect {
|
||||
|
||||
const initialState: ServersMap = {};
|
||||
|
||||
const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
||||
if ('id' in server) {
|
||||
return server;
|
||||
}
|
||||
|
||||
return { ...server, id: randomUUID() };
|
||||
};
|
||||
|
||||
const serversListToMap = (servers: ServerWithId[]): ServersMap => servers.reduce<ServersMap>(
|
||||
(acc, server) => ({ ...acc, [server.id]: server }),
|
||||
{},
|
||||
);
|
||||
|
||||
export const { actions, reducer } = createSlice({
|
||||
name: 'shlink/servers',
|
||||
initialState,
|
||||
|
||||
Reference in New Issue
Block a user