Migrate DuplicatedServersModal to tailwind

This commit is contained in:
Alejandro Celaya
2025-04-05 11:15:42 +02:00
parent 7879476739
commit d8a42b4c3a
6 changed files with 92 additions and 77 deletions

View File

@@ -71,7 +71,7 @@ const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers
<NoMenuLayout>
<ServerForm title="Add new server" onSubmit={onSubmit}>
{!hasServers && (
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onError={setErrorImporting} />
)}
{hasServers && <Button variant="secondary" onClick={goBack}>Cancel</Button>}
<Button type="submit">Create server</Button>
@@ -81,10 +81,10 @@ const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers
{errorImporting && <ImportResult variant="error" />}
<DuplicatedServersModal
isOpen={isConfirmModalOpen}
open={isConfirmModalOpen}
duplicatedServers={serverData ? [serverData] : []}
onDiscard={goBack}
onSave={() => serverData && saveNewServer(serverData)}
onClose={goBack}
onConfirm={() => serverData && saveNewServer(serverData)}
/>
</NoMenuLayout>
);

View File

@@ -48,7 +48,7 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
<div className="tw:flex tw:flex-col tw:md:flex-row tw:gap-2">
<div className="tw:flex tw:gap-2">
<ImportServersBtn className="tw:flex-grow" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
<ImportServersBtn className="tw:flex-grow" onError={setErrorImporting}>Import servers</ImportServersBtn>
{filteredServers.length > 0 && (
<Button variant="secondary" className="tw:flex-grow" onClick={async () => serversExporter.exportServers()}>
<FontAwesomeIcon icon={exportIcon} /> Export servers

View File

@@ -1,41 +1,42 @@
import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { FC } from 'react';
import { Fragment } from 'react';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import type { ServerData } from '../data';
interface DuplicatedServersModalProps {
export type DuplicatedServersModalProps = {
duplicatedServers: ServerData[];
isOpen: boolean;
onDiscard: () => void;
onSave: () => void;
}
open: boolean;
onClose: () => void;
onConfirm: () => void;
};
export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
{ isOpen, duplicatedServers, onDiscard, onSave },
{ open, duplicatedServers, onClose, onConfirm },
) => {
const hasMultipleServers = duplicatedServers.length > 1;
return (
<Modal centered isOpen={isOpen}>
<ModalHeader>Duplicated server{hasMultipleServers && 's'}</ModalHeader>
<ModalBody>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul>
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
<Fragment key={index}>
<li>URL: <b>{url}</b></li>
<li>API key: <b>{apiKey}</b></li>
</Fragment>
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
</ul>
<span>
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?
</span>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={onDiscard}>{hasMultipleServers ? 'Ignore duplicates' : 'Discard'}</Button>
<Button color="primary" onClick={onSave}>Save anyway</Button>
</ModalFooter>
</Modal>
<CardModal
size="lg"
title={`Duplicated server${hasMultipleServers ? 's' : ''}`}
open={open}
onClose={onClose}
onConfirm={onConfirm}
confirmText={`Save duplicate${hasMultipleServers ? 's' : ''}`}
cancelText={hasMultipleServers ? 'Ignore duplicates' : 'Discard'}
>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul className="tw:list-disc tw:mt-4">
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
<Fragment key={index}>
<li>URL: <b>{url}</b></li>
<li>API key: <b>{apiKey}</b></li>
</Fragment>
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
</ul>
<span>
{hasMultipleServers ? 'Do you want to save duplicated servers' : 'Do you want to save this server'}?
</span>
</CardModal>
);
};

View File

@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { ChangeEvent, PropsWithChildren } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef , useState } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import type { FCWithDeps } from '../../container/utils';
import { componentFactory, useDependencies } from '../../container/utils';
@@ -14,7 +14,7 @@ import { dedupServers, ensureUniqueIds } from './index';
export type ImportServersBtnProps = PropsWithChildren<{
onImport?: () => void;
onImportError?: (error: Error) => void;
onError?: (error: Error) => void;
tooltipPlacement?: 'top' | 'bottom';
className?: string;
}>;
@@ -32,8 +32,8 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
createServers,
servers,
children,
onImport = () => {},
onImportError = () => {},
onImport,
onError = () => {},
tooltipPlacement = 'bottom',
className = '',
}) => {
@@ -41,41 +41,46 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
const ref = useElementRef<HTMLInputElement>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle();
const newServersCreatedRef = useRef(false);
const importedServersRef = useRef<ServerWithId[]>([]);
const newServersRef = useRef<ServerWithId[]>([]);
const create = useCallback((serversData: ServerWithId[]) => {
createServers(serversData);
hideModal();
onImport();
}, [createServers, hideModal, onImport]);
const onFile = useCallback(
async ({ target }: ChangeEvent<HTMLInputElement>) =>
serversImporter.importServersFromFile(target.files?.[0])
.then((importedServers) => {
const { duplicatedServers, newServers } = dedupServers(servers, importedServers);
importedServersRef.current = ensureUniqueIds(servers, importedServers);
newServersRef.current = ensureUniqueIds(servers, newServers);
// Immediately create new servers
newServersCreatedRef.current = newServers.length > 0;
createServers(ensureUniqueIds(servers, newServers));
if (duplicatedServers.length === 0) {
create(importedServersRef.current);
} else {
// For duplicated servers, ask for confirmation
if (duplicatedServers.length > 0) {
setDuplicatedServers(duplicatedServers);
showModal();
} else {
onImport?.();
}
})
.then(() => {
// Reset file input after processing file
(target as { value: string | null }).value = null;
})
.catch(onImportError),
[create, onImportError, servers, serversImporter, showModal],
.catch(onError),
[createServers, onError, onImport, servers, serversImporter, showModal],
);
const createAllServers = useCallback(() => create(importedServersRef.current), [create]);
const createNonDuplicatedServers = useCallback(() => create(newServersRef.current), [create]);
const createDuplicatedServers = useCallback(() => {
createServers(ensureUniqueIds(servers, duplicatedServers));
hideModal();
onImport?.();
}, [createServers, duplicatedServers, hideModal, onImport, servers]);
const discardDuplicatedServers = useCallback(() => {
hideModal();
// If duplicated servers were discarded but some non-duplicated servers were created, call onImport
if (newServersCreatedRef.current) {
onImport?.();
}
}, [hideModal, onImport]);
return (
<>
@@ -97,10 +102,10 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
/>
<DuplicatedServersModal
isOpen={isModalOpen}
open={isModalOpen}
duplicatedServers={duplicatedServers}
onDiscard={createNonDuplicatedServers}
onSave={createAllServers}
onClose={discardDuplicatedServers}
onConfirm={createDuplicatedServers}
/>
</>
);