mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-27 04:06:39 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e438f9814 | ||
|
|
ae014e2a14 | ||
|
|
5f5b66a21e | ||
|
|
723e8696af | ||
|
|
32c3c9955c | ||
|
|
847a23ddeb | ||
|
|
08e3e01526 | ||
|
|
82b26add88 | ||
|
|
e3ab34eae1 | ||
|
|
65d9a4125f | ||
|
|
d05344eea7 | ||
|
|
47a2d981c8 | ||
|
|
2229573101 | ||
|
|
a8059c6f1f | ||
|
|
51c1963f4b | ||
|
|
8c10daeddd | ||
|
|
d168e98607 | ||
|
|
a45d990eb7 | ||
|
|
31ae43708a | ||
|
|
cefe327251 | ||
|
|
22280e9d89 | ||
|
|
c73ece41f0 | ||
|
|
7825f7666f | ||
|
|
fc6901ee65 | ||
|
|
1794a07204 | ||
|
|
7215d188a2 | ||
|
|
94890da48f | ||
|
|
078c5c8889 | ||
|
|
23a0cb1245 | ||
|
|
b4015bd8d1 | ||
|
|
3cffaa6e32 | ||
|
|
e68faa63c5 | ||
|
|
e0706a6a89 | ||
|
|
adcb405643 | ||
|
|
63df7db2f5 | ||
|
|
04eb1b1b0e | ||
|
|
40bf57882a | ||
|
|
9a798c20c0 | ||
|
|
45a9783e04 | ||
|
|
ac95ff0166 | ||
|
|
a0df3eeb93 | ||
|
|
e8c5ac8320 | ||
|
|
453d245924 |
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -12,11 +12,12 @@ updates:
|
|||||||
fontawesome:
|
fontawesome:
|
||||||
patterns:
|
patterns:
|
||||||
- '@fortawesome/*'
|
- '@fortawesome/*'
|
||||||
eslint-plugins: # TODO Add eslint back once updated to v9
|
eslint:
|
||||||
patterns:
|
patterns:
|
||||||
- '@shlinkio/eslint-config-js-coding-standard'
|
- '@shlinkio/eslint-config-js-coding-standard'
|
||||||
- 'typescript-eslint'
|
- 'typescript-eslint'
|
||||||
- '*eslint-plugin*'
|
- '*eslint-plugin*'
|
||||||
|
- 'eslint'
|
||||||
shlink:
|
shlink:
|
||||||
patterns:
|
patterns:
|
||||||
- '@shlinkio/*'
|
- '@shlinkio/*'
|
||||||
|
|||||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -4,6 +4,44 @@ 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).
|
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).
|
||||||
|
|
||||||
|
## [4.2.2] - 2024-10-19
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Update to `@shlinkio/shlink-frontend-kit` 0.6.0
|
||||||
|
* Update to `@shlinkio/shlink-web-component` 0.10.1
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [shlink-web-component#475](https://github.com/shlinkio/shlink-web-component/issues/475) Fix incorrect amount of dots being displayed in line charts when the difference in days/weeks/months is rounded up.
|
||||||
|
|
||||||
|
|
||||||
|
## [4.2.1] - 2024-10-09
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1325](https://github.com/shlinkio/shlink-web-client/issues/1325) Get dependency on `uuid` package back, as `crypto.randomUUID()` can only be used in [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
|
||||||
|
* [shlink-web-component#461](https://github.com/shlinkio/shlink-web-component/issues/461) Ensure `shortUrlsList.confirmDeletion` setting is `true` in any case, except when explicitly set to `false`.
|
||||||
|
* [shlink-web-component#237](https://github.com/shlinkio/shlink-web-component/issues/237) Set darker color for previous period in charts, when light theme is enabled.
|
||||||
|
* [shlink-web-component#246](https://github.com/shlinkio/shlink-web-component/issues/246) Fix selected date range not reflected in visits comparison date range selector, when selecting it in the line chart via drag'n'drop.
|
||||||
|
|
||||||
|
|
||||||
## [4.2.0] - 2024-10-07
|
## [4.2.0] - 2024-10-07
|
||||||
### Added
|
### Added
|
||||||
* [shlink-web-component#411](https://github.com/shlinkio/shlink-web-component/issues/411) Add support for `ip-address` redirect conditions when Shlink server is >=4.2
|
* [shlink-web-component#411](https://github.com/shlinkio/shlink-web-component/issues/411) Add support for `ip-address` redirect conditions when Shlink server is >=4.2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.9-alpine as node
|
FROM node:23.0-alpine as node
|
||||||
COPY . /shlink-web-client
|
COPY . /shlink-web-client
|
||||||
ARG VERSION="latest"
|
ARG VERSION="latest"
|
||||||
ENV VERSION ${VERSION}
|
ENV VERSION ${VERSION}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
[](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
[](https://github.com/shlinkio/shlink-web-client/blob/main/LICENSE)
|
||||||
|
|
||||||
[](https://fosstodon.org/@shlinkio)
|
[](https://fosstodon.org/@shlinkio)
|
||||||
[](https://twitter.com/shlinkio)
|
|
||||||
[](https://bsky.app/profile/shlinkio.bsky.social)
|
[](https://bsky.app/profile/shlinkio.bsky.social)
|
||||||
[](https://slnk.to/donate)
|
[](https://slnk.to/donate)
|
||||||
|
|
||||||
|
|||||||
4311
package-lock.json
generated
4311
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -31,11 +31,11 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@reduxjs/toolkit": "^2.2.7",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
"@shlinkio/data-manipulation": "^1.0.3",
|
"@shlinkio/data-manipulation": "^1.0.3",
|
||||||
"@shlinkio/shlink-frontend-kit": "^0.5.2",
|
"@shlinkio/shlink-frontend-kit": "^0.6.0",
|
||||||
"@shlinkio/shlink-js-sdk": "^1.2.0",
|
"@shlinkio/shlink-js-sdk": "^1.2.0",
|
||||||
"@shlinkio/shlink-web-component": "^0.8.0",
|
"@shlinkio/shlink-web-component": "^0.10.1",
|
||||||
"bootstrap": "5.2.3",
|
"bootstrap": "5.2.3",
|
||||||
"bottlejs": "^2.0.1",
|
"bottlejs": "^2.0.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -46,9 +46,10 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-external-link": "^2.3.1",
|
"react-external-link": "^2.3.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.27.0",
|
||||||
"reactstrap": "^9.2.3",
|
"reactstrap": "^9.2.3",
|
||||||
"redux-localstorage-simple": "^2.5.1",
|
"redux-localstorage-simple": "^2.5.1",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
"workbox-core": "^7.1.0",
|
"workbox-core": "^7.1.0",
|
||||||
"workbox-expiration": "^7.1.0",
|
"workbox-expiration": "^7.1.0",
|
||||||
"workbox-precaching": "^7.1.0",
|
"workbox-precaching": "^7.1.0",
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
"workbox-strategies": "^7.1.0"
|
"workbox-strategies": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~3.1.0",
|
"@shlinkio/eslint-config-js-coding-standard": "~3.2.0",
|
||||||
"@shlinkio/stylelint-config-css-coding-standard": "~1.1.1",
|
"@shlinkio/stylelint-config-css-coding-standard": "~1.1.1",
|
||||||
"@stylistic/eslint-plugin": "^2.9.0",
|
"@stylistic/eslint-plugin": "^2.9.0",
|
||||||
"@testing-library/jest-dom": "^6.5.0",
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
@@ -64,25 +65,25 @@
|
|||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@total-typescript/shoehorn": "^0.1.2",
|
"@total-typescript/shoehorn": "^0.1.2",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"@vitest/coverage-v8": "^2.1.3",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"axe-core": "^4.10.0",
|
"axe-core": "^4.10.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.13.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||||
"eslint-plugin-react": "^7.37.1",
|
"eslint-plugin-react": "^7.37.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"sass": "^1.79.4",
|
"sass": "^1.80.3",
|
||||||
"stylelint": "^15.11.0",
|
"stylelint": "^15.11.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.8.0",
|
"typescript-eslint": "^8.10.0",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.9",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vitest": "^2.0.2"
|
"vitest": "^2.0.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export function componentFactory<Deps, CompType = Omit<Partial<Deps>, keyof FC>>
|
|||||||
console.error(`[Debug] Could not find "${dep as string}" dependency in container`);
|
console.error(`[Debug] Could not find "${dep as string}" dependency in container`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
Component[dep] = resolvedDependency;
|
Component[dep] = resolvedDependency;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import './index.scss';
|
|||||||
const store = setUpStore(container);
|
const store = setUpStore(container);
|
||||||
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
|
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
createRoot(document.getElementById('root')!).render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter basename={pack.homepage}>
|
<BrowserRouter basename={pack.homepage}>
|
||||||
<ErrorHandler>
|
<ErrorHandler>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { Result, useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { Result, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import { useGoBack } from '../utils/helpers/hooks';
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
|
import { randomUUID } from '../utils/utils';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from './data';
|
import type { ServerData, ServersMap, ServerWithId } from './data';
|
||||||
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
||||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||||
@@ -44,31 +45,28 @@ const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers
|
|||||||
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
|
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
|
||||||
const [serverData, setServerData] = useState<ServerData>();
|
const [serverData, setServerData] = useState<ServerData>();
|
||||||
const saveNewServer = useCallback((theServerData: ServerData) => {
|
const saveNewServer = useCallback((theServerData: ServerData) => {
|
||||||
const id = crypto.randomUUID();
|
const id = randomUUID();
|
||||||
|
|
||||||
createServers([{ ...theServerData, id }]);
|
createServers([{ ...theServerData, id }]);
|
||||||
navigate(`/server/${id}`);
|
navigate(`/server/${id}`);
|
||||||
}, [createServers, navigate]);
|
}, [createServers, navigate]);
|
||||||
|
const onSubmit = useCallback((newServerData: ServerData) => {
|
||||||
useEffect(() => {
|
setServerData(newServerData);
|
||||||
if (!serverData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverExists = Object.values(servers).some(
|
const serverExists = Object.values(servers).some(
|
||||||
({ url, apiKey }) => serverData?.url === url && serverData?.apiKey === apiKey,
|
({ url, apiKey }) => newServerData.url === url && newServerData.apiKey === apiKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverExists) {
|
if (serverExists) {
|
||||||
toggleConfirmModal();
|
toggleConfirmModal();
|
||||||
} else {
|
} else {
|
||||||
saveNewServer(serverData);
|
saveNewServer(newServerData);
|
||||||
}
|
}
|
||||||
}, [saveNewServer, serverData, servers, toggleConfirmModal]);
|
}, [saveNewServer, servers, toggleConfirmModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={setServerData}>
|
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={onSubmit}>
|
||||||
{!hasServers && (
|
{!hasServers && (
|
||||||
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
|
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { Result, SearchField, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { Result, SearchField, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, Row } from 'reactstrap';
|
import { Button, Row } from 'reactstrap';
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
@@ -34,26 +34,23 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
|
|||||||
useTimeoutToggle,
|
useTimeoutToggle,
|
||||||
ManageServersRow,
|
ManageServersRow,
|
||||||
} = useDependencies(ManageServers);
|
} = useDependencies(ManageServers);
|
||||||
const allServers = Object.values(servers);
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [serversList, setServersList] = useState(allServers);
|
const allServers = useMemo(() => Object.values(servers), [servers]);
|
||||||
const filterServers = (searchTerm: string) => setServersList(
|
const filteredServers = useMemo(
|
||||||
allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
|
() => allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())),
|
||||||
|
[allServers, searchTerm],
|
||||||
);
|
);
|
||||||
const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect);
|
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
|
||||||
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setServersList(Object.values(servers));
|
|
||||||
}, [servers]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<SearchField className="mb-3" onChange={filterServers} />
|
<SearchField className="mb-3" onChange={setSearchTerm} />
|
||||||
|
|
||||||
<Row className="mb-3">
|
<Row className="mb-3">
|
||||||
<div className="col-md-6 d-flex d-md-block mb-2 mb-md-0">
|
<div className="col-md-6 d-flex d-md-block mb-2 mb-md-0">
|
||||||
<ImportServersBtn className="flex-fill" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
|
<ImportServersBtn className="flex-fill" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
|
||||||
{allServers.length > 0 && (
|
{filteredServers.length > 0 && (
|
||||||
<Button outline className="ms-2 flex-fill" onClick={async () => serversExporter.exportServers()}>
|
<Button outline className="ms-2 flex-fill" onClick={async () => serversExporter.exportServers()}>
|
||||||
<FontAwesomeIcon icon={exportIcon} fixedWidth /> Export servers
|
<FontAwesomeIcon icon={exportIcon} fixedWidth /> Export servers
|
||||||
</Button>
|
</Button>
|
||||||
@@ -77,8 +74,8 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{!serversList.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
|
{!filteredServers.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
|
||||||
{serversList.map((server) => (
|
{filteredServers.map((server) => (
|
||||||
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
|
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Reset input after processing file
|
// Reset input after processing file
|
||||||
(target as { value: string | null }).value = null; // eslint-disable-line no-param-reassign
|
(target as { value: string | null }).value = null;
|
||||||
})
|
})
|
||||||
.catch(onImportError),
|
.catch(onImportError),
|
||||||
[create, onImportError, servers, serversImporter, showModal],
|
[create, onImportError, servers, serversImporter, showModal],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InputFormGroup, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { InputFormGroup, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
import type { ServerData } from '../data';
|
import type { ServerData } from '../data';
|
||||||
|
|
||||||
@@ -11,19 +11,11 @@ type ServerFormProps = PropsWithChildren<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
|
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children, title }) => {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState(initialValues?.name ?? '');
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState(initialValues?.url ?? '');
|
||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState(initialValues?.apiKey ?? '');
|
||||||
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialValues) {
|
|
||||||
setName(initialValues.name);
|
|
||||||
setUrl(initialValues.url);
|
|
||||||
setApiKey(initialValues.apiKey);
|
|
||||||
}
|
|
||||||
}, [initialValues]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="server-form" name="serverForm" onSubmit={handleSubmit}>
|
<form className="server-form" name="serverForm" onSubmit={handleSubmit}>
|
||||||
<SimpleCard className="mb-3" title={title}>
|
<SimpleCard className="mb-3" title={title}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { randomUUID } from '../../utils/utils';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||||
|
|
||||||
interface EditServer {
|
interface EditServer {
|
||||||
@@ -19,7 +20,7 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
|||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...server, id: crypto.randomUUID() };
|
return { ...server, id: randomUUID() };
|
||||||
};
|
};
|
||||||
|
|
||||||
const serversListToMap = (servers: ServerWithId[]): ServersMap => servers.reduce<ServersMap>(
|
const serversListToMap = (servers: ServerWithId[]): ServersMap => servers.reduce<ServersMap>(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class ServersExporter {
|
|||||||
saveCsv(this.window, csv, SERVERS_FILENAME);
|
saveCsv(this.window, csv, SERVERS_FILENAME);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// FIXME Handle error
|
// FIXME Handle error
|
||||||
console.error(e); // eslint-disable-line no-console
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ShlinkState } from '../../container/types';
|
import type { ShlinkState } from '../../container/types';
|
||||||
|
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
export const migrateDeprecatedSettings = (state: Partial<ShlinkState>): Partial<ShlinkState> => {
|
||||||
if (!state.settings) {
|
if (!state.settings) {
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { SyntheticEvent } from 'react';
|
import type { SyntheticEvent } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const handleEventPreventingDefault = <T>(handler: () => T) => (e: SyntheticEvent) => {
|
export const handleEventPreventingDefault = <T>(handler: () => T) => (e: SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handler();
|
handler();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const randomUUID = () => v4();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('ShlinkApiClientBuilder', () => {
|
|||||||
const apiKey = 'apiKey';
|
const apiKey = 'apiKey';
|
||||||
const apiClient = buildShlinkApiClient(fromPartial({}))(server({ url, apiKey }));
|
const apiClient = buildShlinkApiClient(fromPartial({}))(server({ url, apiKey }));
|
||||||
|
|
||||||
expect(apiClient['serverInfo'].baseUrl).toEqual(url); // eslint-disable-line @typescript-eslint/dot-notation
|
expect(apiClient['serverInfo'].baseUrl).toEqual(url);
|
||||||
expect(apiClient['serverInfo'].apiKey).toEqual(apiKey); // eslint-disable-line @typescript-eslint/dot-notation
|
expect(apiClient['serverInfo'].apiKey).toEqual(apiKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
selectedServerReducerCreator,
|
selectedServerReducerCreator,
|
||||||
selectServer as selectServerCreator,
|
selectServer as selectServerCreator,
|
||||||
} from '../../../src/servers/reducers/selectedServer';
|
} from '../../../src/servers/reducers/selectedServer';
|
||||||
|
import { randomUUID } from '../../../src/utils/utils';
|
||||||
|
|
||||||
describe('selectedServerReducer', () => {
|
describe('selectedServerReducer', () => {
|
||||||
const dispatch = vi.fn();
|
const dispatch = vi.fn();
|
||||||
@@ -40,7 +41,7 @@ describe('selectedServerReducer', () => {
|
|||||||
['latest', MAX_FALLBACK_VERSION, 'latest'],
|
['latest', MAX_FALLBACK_VERSION, 'latest'],
|
||||||
['%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%'],
|
['%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%'],
|
||||||
])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => {
|
])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => {
|
||||||
const id = crypto.randomUUID();
|
const id = randomUUID();
|
||||||
const getState = createGetStateMock(id);
|
const getState = createGetStateMock(id);
|
||||||
const expectedSelectedServer = {
|
const expectedSelectedServer = {
|
||||||
id,
|
id,
|
||||||
@@ -59,7 +60,7 @@ describe('selectedServerReducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches error when health endpoint fails', async () => {
|
it('dispatches error when health endpoint fails', async () => {
|
||||||
const id = crypto.randomUUID();
|
const id = randomUUID();
|
||||||
const getState = createGetStateMock(id);
|
const getState = createGetStateMock(id);
|
||||||
const expectedSelectedServer = fromPartial<NonReachableServer>({ id, serverNotReachable: true });
|
const expectedSelectedServer = fromPartial<NonReachableServer>({ id, serverNotReachable: true });
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ describe('selectedServerReducer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches error when server is not found', async () => {
|
it('dispatches error when server is not found', async () => {
|
||||||
const id = crypto.randomUUID();
|
const id = randomUUID();
|
||||||
const getState = vi.fn(() => fromPartial<ShlinkState>({ servers: {} }));
|
const getState = vi.fn(() => fromPartial<ShlinkState>({ servers: {} }));
|
||||||
const expectedSelectedServer: NotFoundServer = { serverNotFound: true };
|
const expectedSelectedServer: NotFoundServer = { serverNotFound: true };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user