Migrate from ESLint to Oxlint and Oxfmt

This commit is contained in:
Alejandro Celaya
2026-07-26 09:12:18 +02:00
parent 2cf85a18b3
commit 54d4d9ebad
81 changed files with 2004 additions and 3790 deletions

View File

@@ -38,25 +38,31 @@ const CreateServerBase: FC<CreateServerProps> = withoutSelectedServer(({ useTime
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
const { flag: isConfirmModalOpen, toggle: toggleConfirmModal } = useToggle();
const [serverData, setServerData] = useState<ServerData>();
const saveNewServer = useCallback((newServerData: ServerData) => {
const [newServerWithUniqueId] = ensureUniqueIds(servers, [newServerData]);
const saveNewServer = useCallback(
(newServerData: ServerData) => {
const [newServerWithUniqueId] = ensureUniqueIds(servers, [newServerData]);
createServers([newServerWithUniqueId]);
navigate(`/server/${newServerWithUniqueId.id}`);
}, [createServers, navigate, servers]);
const onSubmit = useCallback((newServerData: ServerData) => {
setServerData(newServerData);
createServers([newServerWithUniqueId]);
navigate(`/server/${newServerWithUniqueId.id}`);
},
[createServers, navigate, servers],
);
const onSubmit = useCallback(
(newServerData: ServerData) => {
setServerData(newServerData);
const serverExists = Object.values(servers).some(
({ url, apiKey }) => newServerData.url === url && newServerData.apiKey === apiKey,
);
const serverExists = Object.values(servers).some(
({ url, apiKey }) => newServerData.url === url && newServerData.apiKey === apiKey,
);
if (serverExists) {
toggleConfirmModal();
} else {
saveNewServer(newServerData);
}
}, [saveNewServer, servers, toggleConfirmModal]);
if (serverExists) {
toggleConfirmModal();
} else {
saveNewServer(newServerData);
}
},
[saveNewServer, servers, toggleConfirmModal],
);
return (
<NoMenuLayout>
@@ -64,7 +70,11 @@ const CreateServerBase: FC<CreateServerProps> = withoutSelectedServer(({ useTime
{!hasServers && (
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onError={setErrorImporting} />
)}
{hasServers && <Button variant="secondary" onClick={goBack}>Cancel</Button>}
{hasServers && (
<Button variant="secondary" onClick={goBack}>
Cancel
</Button>
)}
<Button type="submit">Create server</Button>
</ServerForm>

View File

@@ -12,12 +12,15 @@ export type DeleteServerButtonProps = PropsWithChildren<{
export const DeleteServerButton: FC<DeleteServerButtonProps> = ({ server, children }) => {
const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle();
const navigate = useNavigate();
const onClose = useCallback((confirmed: boolean) => {
hideModal();
if (confirmed) {
navigate('/');
}
}, [hideModal, navigate]);
const onClose = useCallback(
(confirmed: boolean) => {
hideModal();
if (confirmed) {
navigate('/');
}
},
[hideModal, navigate],
);
return (
<>

View File

@@ -13,11 +13,14 @@ export type DeleteServerModalProps = {
export const DeleteServerModal: FC<DeleteServerModalProps> = ({ server, onClose, open }) => {
const { deleteServer } = useServers();
const onClosed = useCallback((exitAction: ExitAction) => {
if (exitAction === 'confirm') {
deleteServer(server);
}
}, [deleteServer, server]);
const onClosed = useCallback(
(exitAction: ExitAction) => {
if (exitAction === 'confirm') {
deleteServer(server);
}
},
[deleteServer, server],
);
return (
<CardModal
@@ -30,11 +33,13 @@ export const DeleteServerModal: FC<DeleteServerModalProps> = ({ server, onClose,
confirmText="Delete"
>
<div className="flex flex-col gap-y-4">
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
<p>
Are you sure you want to remove <b>{server ? server.name : ''}</b>?
</p>
<p>
<i>
No data will be deleted, only the access to this server will be removed from this device.
You can create it again at any moment.
No data will be deleted, only the access to this server will be removed from this device. You can create it
again at any moment.
</i>
</p>
</div>

View File

@@ -34,7 +34,9 @@ export const EditServer: FC = withSelectedServer(() => {
initialValues={selectedServer}
onSubmit={handleSubmit}
>
<Button variant="secondary" onClick={goBack}>Cancel</Button>
<Button variant="secondary" onClick={goBack}>
Cancel
</Button>
<Button type="submit">Save</Button>
</ServerForm>
</NoMenuLayout>

View File

@@ -19,66 +19,75 @@ export type ManageServersProps = {
const SHOW_IMPORT_MSG_TIME = 4000;
const ManageServersBase: FC<ManageServersProps> = withoutSelectedServer(({
ServersExporter: serversExporter,
useTimeoutToggle,
}) => {
const { servers } = useServers();
const [searchTerm, setSearchTerm] = useState('');
const allServers = useMemo(() => Object.values(servers), [servers]);
const filteredServers = useMemo(
() => allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
[allServers, searchTerm],
);
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
const ManageServersBase: FC<ManageServersProps> = withoutSelectedServer(
({ ServersExporter: serversExporter, useTimeoutToggle }) => {
const { servers } = useServers();
const [searchTerm, setSearchTerm] = useState('');
const allServers = useMemo(() => Object.values(servers), [servers]);
const filteredServers = useMemo(
() => allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
[allServers, searchTerm],
);
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
return (
<NoMenuLayout className="flex flex-col gap-y-4">
<SearchInput onChange={setSearchTerm} />
return (
<NoMenuLayout className="flex flex-col gap-y-4">
<SearchInput onChange={setSearchTerm} />
<div className="flex flex-col md:flex-row gap-2">
<div className="flex gap-2">
<ImportServersBtn className="flex-grow" onError={setErrorImporting}>Import servers</ImportServersBtn>
{filteredServers.length > 0 && (
<Button variant="secondary" className="flex-grow" onClick={async () => serversExporter.exportServers()}>
<FontAwesomeIcon icon={exportIcon} widthAuto /> Export servers
</Button>
)}
</div>
<Button className="md:ml-auto" to="/server/create">
<FontAwesomeIcon icon={plusIcon} widthAuto /> Add a server
</Button>
</div>
<SimpleCard className="card">
<Table header={(
<Table.Row>
{hasAutoConnect && (
<Table.Cell className="w-[35px]"><span className="sr-only">Auto-connect</span></Table.Cell>
<div className="flex flex-col md:flex-row gap-2">
<div className="flex gap-2">
<ImportServersBtn className="flex-grow" onError={setErrorImporting}>
Import servers
</ImportServersBtn>
{filteredServers.length > 0 && (
<Button variant="secondary" className="flex-grow" onClick={async () => serversExporter.exportServers()}>
<FontAwesomeIcon icon={exportIcon} widthAuto /> Export servers
</Button>
)}
<Table.Cell>Name</Table.Cell>
<Table.Cell>Base URL</Table.Cell>
<Table.Cell><span className="sr-only">Options</span></Table.Cell>
</Table.Row>
)}>
{!filteredServers.length && (
<Table.Row className="text-center"><Table.Cell colSpan={4}>No servers found.</Table.Cell></Table.Row>
)}
{filteredServers.map((server) => (
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
))}
</Table>
</SimpleCard>
{errorImporting && (
<div>
<Result variant="error">The servers could not be imported. Make sure the format is correct.</Result>
</div>
<Button className="md:ml-auto" to="/server/create">
<FontAwesomeIcon icon={plusIcon} widthAuto /> Add a server
</Button>
</div>
)}
</NoMenuLayout>
);
});
<SimpleCard className="card">
<Table
header={
<Table.Row>
{hasAutoConnect && (
<Table.Cell className="w-[35px]">
<span className="sr-only">Auto-connect</span>
</Table.Cell>
)}
<Table.Cell>Name</Table.Cell>
<Table.Cell>Base URL</Table.Cell>
<Table.Cell>
<span className="sr-only">Options</span>
</Table.Cell>
</Table.Row>
}
>
{!filteredServers.length && (
<Table.Row className="text-center">
<Table.Cell colSpan={4}>No servers found.</Table.Cell>
</Table.Row>
)}
{filteredServers.map((server) => (
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
))}
</Table>
</SimpleCard>
{errorImporting && (
<div>
<Result variant="error">The servers could not be imported. Make sure the format is correct.</Result>
</div>
)}
</NoMenuLayout>
);
},
);
export const ManageServers = withDependencies(ManageServersBase, ['ServersExporter', 'useTimeoutToggle']);

View File

@@ -34,7 +34,9 @@ export const ManageServersRow: FC<ManageServersRowProps> = ({ server, hasAutoCon
<Table.Cell className="font-bold" columnName="Name">
<Link to={`/server/${server.id}`}>{server.name}</Link>
</Table.Cell>
<Table.Cell columnName="Base URL" className="max-lg:border-b-0">{server.url}</Table.Cell>
<Table.Cell columnName="Base URL" className="max-lg:border-b-0">
{server.url}
</Table.Cell>
<Table.Cell className="text-right max-lg:absolute right-0 -top-1 mx-lg:pt-0">
<ManageServersRowDropdown server={server} />
</Table.Cell>

View File

@@ -12,11 +12,13 @@ export const ServersDropdown: FC = () => {
const { selectedServer } = useSelectedServer();
return (
<NavBar.Dropdown buttonContent={(
<span className="flex items-center gap-1.5">
<FontAwesomeIcon icon={serverIcon} /> Servers
</span>
)}>
<NavBar.Dropdown
buttonContent={
<span className="flex items-center gap-1.5">
<FontAwesomeIcon icon={serverIcon} /> Servers
</span>
}
>
{serversList.length === 0 ? (
<Dropdown.Item to="/server/create">
<FontAwesomeIcon icon={plusIcon} /> Add a server

View File

@@ -25,7 +25,7 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => (
</Link>
);
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, borderless }) => (
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, borderless }) =>
servers.length > 0 && (
<div
data-testid="list"
@@ -35,7 +35,8 @@ export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, borderles
{ 'border-y': !borderless },
)}
>
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
{servers.map(({ id, name }) => (
<ServerListItem key={id} id={id} name={name} />
))}
</div>
)
);
);

View File

@@ -10,9 +10,12 @@ export type DuplicatedServersModalProps = {
onConfirm: () => void;
};
export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
{ open, duplicatedServers, onClose, onConfirm },
) => {
export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = ({
open,
duplicatedServers,
onClose,
onConfirm,
}) => {
const hasMultipleServers = duplicatedServers.length > 1;
return (
@@ -27,16 +30,24 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul className="list-disc my-4 pl-5">
{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>))}
{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>
<span>{hasMultipleServers ? 'Do you want to save duplicated servers' : 'Do you want to save this server'}?</span>
</CardModal>
);
};

View File

@@ -17,7 +17,7 @@ export type ImportServersBtnProps = PropsWithChildren<{
className?: string;
// Injected
ServersImporter: ServersImporter
ServersImporter: ServersImporter;
}>;
const ImportServersBtnBase: FC<ImportServersBtnProps> = ({
@@ -37,7 +37,8 @@ const ImportServersBtnBase: FC<ImportServersBtnProps> = ({
const onFile = useCallback(
async ({ target }: ChangeEvent<HTMLInputElement>) =>
serversImporter.importServersFromFile(target.files?.[0])
serversImporter
.importServersFromFile(target.files?.[0])
.then((importedServers) => {
const { duplicatedServers, newServers } = dedupServers(servers, importedServers);

View File

@@ -26,8 +26,8 @@ export const ServerError: FC = () => {
</Message>
<p className="text-xl">
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
These are the Shlink servers currently configured. Choose one of them or{' '}
<Link to="/server/create">add a new one</Link>.
</p>
<Card className="w-full max-w-100 overflow-hidden">
<ServersListGroup borderless servers={Object.values(servers)} />
@@ -35,9 +35,8 @@ export const ServerError: FC = () => {
{isServerWithId(selectedServer) && (
<p className="text-xl">
Alternatively, if you think you may have misconfigured this server, you
can <DeleteServerButton server={selectedServer}>remove
it</DeleteServerButton> or&nbsp;
Alternatively, if you think you may have misconfigured this server, you can{' '}
<DeleteServerButton server={selectedServer}>remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
</p>
)}

View File

@@ -45,13 +45,13 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
Forward credentials to this server on every request.
</Label>
<small className="pl-5.5 text-gray-600 dark:text-gray-400 mt-0.5">
{'"'}Credentials{'"'} here means cookies, TLS client certificates, or authentication headers containing a username
and password.
{'"'}Credentials{'"'} here means cookies, TLS client certificates, or authentication headers containing a
username and password.
</small>
<small className="pl-5.5 text-gray-600 dark:text-gray-400">
<b>Important!</b> If you are not sure what this means, leave it unchecked. Enabling this option will
make all requests fail for Shlink older than v4.5.0, as it requires the server to set a more strict
value for <code className="whitespace-nowrap">Access-Control-Allow-Origin</code> than <code>*</code>.
<b>Important!</b> If you are not sure what this means, leave it unchecked. Enabling this option will make
all requests fail for Shlink older than v4.5.0, as it requires the server to set a more strict value for{' '}
<code className="whitespace-nowrap">Access-Control-Allow-Origin</code> than <code>*</code>.
</small>
</div>
</Details>

View File

@@ -49,9 +49,9 @@ export type DedupServersResult = {
*/
export function dedupServers(servers: ServersMap, serversToAdd: ServerData[]): DedupServersResult {
const serversList = Object.values(servers);
const { duplicatedServers = [], newServers = [] } = groupBy(
serversToAdd,
(server) => serversInclude(serversList, server) ? 'duplicatedServers' : 'newServers',
// oxlint-disable-next-line typescript/no-useless-default-assignment - Type definition is wrong
const { duplicatedServers = [], newServers = [] } = groupBy(serversToAdd, (server) =>
serversInclude(serversList, server) ? 'duplicatedServers' : 'newServers',
);
return { duplicatedServers, newServers };

View File

@@ -8,10 +8,7 @@ import { hasServerData } from '../data';
import { ensureUniqueIds } from '../helpers';
import { createServers, useServers } from './servers';
const responseToServersList = (data: any) => ensureUniqueIds(
{},
(Array.isArray(data) ? data.filter(hasServerData) : []),
);
const responseToServersList = (data: any) => ensureUniqueIds({}, Array.isArray(data) ? data.filter(hasServerData) : []);
export const fetchServers = createAsyncThunk(
'shlink/remoteServers/fetchServers',

View File

@@ -15,17 +15,16 @@ export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999';
export const LATEST_VERSION_CONSTRAINT = 'latest';
const versionToSemVer = (version: string) => toSemVer(
version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
MIN_FALLBACK_VERSION,
);
const versionToSemVer = (version: string) =>
toSemVer(version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version, MIN_FALLBACK_VERSION);
const getServerVersion = memoizeWith(
(server: ServerWithId) => `${server.id}_${server.url}_${server.apiKey}`,
async (_server: ServerWithId, health: () => Promise<ShlinkHealth>) => health().then(({ version }) => ({
version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
})),
async (_server: ServerWithId, health: () => Promise<ShlinkHealth>) =>
health().then(({ version }) => ({
version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
})),
);
const initialState: SelectedServer = null;

View File

@@ -27,9 +27,7 @@ export const { actions, reducer: serversReducer } = createSlice({
}),
reducer: (state, { payload }: PayloadAction<EditServer>) => {
const { serverId, serverData } = payload;
return (
!state[serverId] ? state : { ...state, [serverId]: { ...state[serverId], ...serverData } }
);
return !state[serverId] ? state : { ...state, [serverId]: { ...state[serverId], ...serverData } };
},
},
deleteServer: (state, { payload }) => {