mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Merge pull request #950 from acelaya-forks/feature/remove-ramda
Feature/remove ramda
This commit is contained in:
commit
788026f2d1
19 changed files with 79 additions and 98 deletions
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
@ -12,6 +12,9 @@ updates:
|
||||||
fontawesome:
|
fontawesome:
|
||||||
patterns:
|
patterns:
|
||||||
- '@fortawesome*'
|
- '@fortawesome*'
|
||||||
|
shlink:
|
||||||
|
patterns:
|
||||||
|
- '@shlinkio*'
|
||||||
ignore:
|
ignore:
|
||||||
# Bootstrap can introduce visual breaking changes on styles
|
# Bootstrap can introduce visual breaking changes on styles
|
||||||
# Ignore it, since the plan is to remove it anyway
|
# Ignore it, since the plan is to remove it anyway
|
||||||
|
|
40
package-lock.json
generated
40
package-lock.json
generated
|
@ -15,6 +15,7 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@json2csv/plainjs": "^7.0.3",
|
"@json2csv/plainjs": "^7.0.3",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
|
"@shlinkio/data-manipulation": "^1.0.1",
|
||||||
"@shlinkio/shlink-frontend-kit": "^0.4.0",
|
"@shlinkio/shlink-frontend-kit": "^0.4.0",
|
||||||
"@shlinkio/shlink-js-sdk": "^0.1.0",
|
"@shlinkio/shlink-js-sdk": "^0.1.0",
|
||||||
"@shlinkio/shlink-web-component": "^0.3.5",
|
"@shlinkio/shlink-web-component": "^0.3.5",
|
||||||
|
@ -24,7 +25,6 @@
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"csvtojson": "^2.0.10",
|
"csvtojson": "^2.0.10",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"ramda": "^0.27.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-external-link": "^2.2.0",
|
"react-external-link": "^2.2.0",
|
||||||
|
@ -46,7 +46,6 @@
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@total-typescript/shoehorn": "^0.1.1",
|
"@total-typescript/shoehorn": "^0.1.1",
|
||||||
"@types/ramda": "^0.27.66",
|
|
||||||
"@types/react": "^18.2.33",
|
"@types/react": "^18.2.33",
|
||||||
"@types/react-dom": "^18.2.14",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.6",
|
||||||
|
@ -2790,6 +2789,11 @@
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@shlinkio/data-manipulation": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LYmRXcI2CPi8RZfI5wE7lf0Bg74NiPK+y2wuePvRlQtQud2jDZTbCmCIe6fyQV4NGrpKTn+D4NjcmVT/Iq2InQ=="
|
||||||
|
},
|
||||||
"node_modules/@shlinkio/eslint-config-js-coding-standard": {
|
"node_modules/@shlinkio/eslint-config-js-coding-standard": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@shlinkio/eslint-config-js-coding-standard/-/eslint-config-js-coding-standard-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shlinkio/eslint-config-js-coding-standard/-/eslint-config-js-coding-standard-2.3.0.tgz",
|
||||||
|
@ -3272,15 +3276,6 @@
|
||||||
"version": "15.7.3",
|
"version": "15.7.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/ramda": {
|
|
||||||
"version": "0.27.66",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz",
|
|
||||||
"integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ts-toolbelt": "^6.15.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.33",
|
"version": "18.2.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
|
||||||
|
@ -9622,11 +9617,6 @@
|
||||||
"typescript": ">=4.2.0"
|
"typescript": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-toolbelt": {
|
|
||||||
"version": "6.15.5",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -12507,6 +12497,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz",
|
||||||
"integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw=="
|
"integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw=="
|
||||||
},
|
},
|
||||||
|
"@shlinkio/data-manipulation": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LYmRXcI2CPi8RZfI5wE7lf0Bg74NiPK+y2wuePvRlQtQud2jDZTbCmCIe6fyQV4NGrpKTn+D4NjcmVT/Iq2InQ=="
|
||||||
|
},
|
||||||
"@shlinkio/eslint-config-js-coding-standard": {
|
"@shlinkio/eslint-config-js-coding-standard": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@shlinkio/eslint-config-js-coding-standard/-/eslint-config-js-coding-standard-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shlinkio/eslint-config-js-coding-standard/-/eslint-config-js-coding-standard-2.3.0.tgz",
|
||||||
|
@ -12853,15 +12848,6 @@
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.3"
|
"version": "15.7.3"
|
||||||
},
|
},
|
||||||
"@types/ramda": {
|
|
||||||
"version": "0.27.66",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.66.tgz",
|
|
||||||
"integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"ts-toolbelt": "^6.15.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "18.2.33",
|
"version": "18.2.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
|
||||||
|
@ -17115,10 +17101,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"ts-toolbelt": {
|
|
||||||
"version": "6.15.5",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"tsconfig-paths": {
|
"tsconfig-paths": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@json2csv/plainjs": "^7.0.3",
|
"@json2csv/plainjs": "^7.0.3",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
|
"@shlinkio/data-manipulation": "^1.0.1",
|
||||||
"@shlinkio/shlink-frontend-kit": "^0.4.0",
|
"@shlinkio/shlink-frontend-kit": "^0.4.0",
|
||||||
"@shlinkio/shlink-js-sdk": "^0.1.0",
|
"@shlinkio/shlink-js-sdk": "^0.1.0",
|
||||||
"@shlinkio/shlink-web-component": "^0.3.5",
|
"@shlinkio/shlink-web-component": "^0.3.5",
|
||||||
|
@ -40,7 +41,6 @@
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"csvtojson": "^2.0.10",
|
"csvtojson": "^2.0.10",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"ramda": "^0.27.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-external-link": "^2.2.0",
|
"react-external-link": "^2.2.0",
|
||||||
|
@ -62,7 +62,6 @@
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@total-typescript/shoehorn": "^0.1.1",
|
"@total-typescript/shoehorn": "^0.1.1",
|
||||||
"@types/ramda": "^0.27.66",
|
|
||||||
"@types/react": "^18.2.33",
|
"@types/react": "^18.2.33",
|
||||||
"@types/react-dom": "^18.2.14",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.6",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, values } from 'ramda';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
@ -16,8 +15,8 @@ interface HomeProps {
|
||||||
|
|
||||||
export const Home = ({ servers }: HomeProps) => {
|
export const Home = ({ servers }: HomeProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const serversList = values(servers);
|
const serversList = Object.values(servers);
|
||||||
const hasServers = !isEmpty(serversList);
|
const hasServers = serversList.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Try to redirect to the first server marked as auto-connect
|
// Try to redirect to the first server marked as auto-connect
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { pipe } from 'ramda';
|
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import type { SelectedServer } from '../servers/data';
|
import type { SelectedServer } from '../servers/data';
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
||||||
|
|
||||||
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
||||||
const normalizeVersion = pipe(versionToSemVer(), versionToPrintable);
|
const normalizeVersion = (version: string) => versionToPrintable(versionToSemVer(version));
|
||||||
|
|
||||||
export interface ShlinkVersionsProps {
|
export interface ShlinkVersionsProps {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { IContainer } from 'bottlejs';
|
import type { IContainer } from 'bottlejs';
|
||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import { pick } from 'ramda';
|
|
||||||
import { connect as reduxConnect } from 'react-redux';
|
import { connect as reduxConnect } from 'react-redux';
|
||||||
import { provideServices as provideApiServices } from '../api/services/provideServices';
|
import { provideServices as provideApiServices } from '../api/services/provideServices';
|
||||||
import { provideServices as provideAppServices } from '../app/services/provideServices';
|
import { provideServices as provideAppServices } from '../app/services/provideServices';
|
||||||
|
@ -18,14 +17,20 @@ export const { container } = bottle;
|
||||||
|
|
||||||
const lazyService = <T extends Function, K>(cont: IContainer, serviceName: string) =>
|
const lazyService = <T extends Function, K>(cont: IContainer, serviceName: string) =>
|
||||||
(...args: any[]) => (cont[serviceName] as T)(...args) as K;
|
(...args: any[]) => (cont[serviceName] as T)(...args) as K;
|
||||||
|
|
||||||
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
const mapActionService = (map: LazyActionMap, actionName: string): LazyActionMap => ({
|
||||||
...map,
|
...map,
|
||||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||||
[actionName]: lazyService(container, actionName),
|
[actionName]: lazyService(container, actionName),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pickProps = (propsToPick: string[]) => (obj: any) => Object.fromEntries(
|
||||||
|
propsToPick.map((key) => [key, obj[key]]),
|
||||||
|
);
|
||||||
|
|
||||||
const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) =>
|
const connect: ConnectDecorator = (propsFromState: string[] | null, actionServiceNames: string[] = []) =>
|
||||||
reduxConnect(
|
reduxConnect(
|
||||||
propsFromState ? pick(propsFromState) : null,
|
propsFromState ? pickProps(propsFromState) : null,
|
||||||
actionServiceNames.reduce(mapActionService, {}),
|
actionServiceNames.reduce(mapActionService, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, values } from 'ramda';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
||||||
import type { SelectedServer, ServersMap } from './data';
|
import type { SelectedServer, ServersMap } from './data';
|
||||||
|
@ -12,10 +11,10 @@ export interface ServersDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProps) => {
|
||||||
const serversList = values(servers);
|
const serversList = Object.values(servers);
|
||||||
|
|
||||||
const renderServers = () => {
|
const renderServers = () => {
|
||||||
if (isEmpty(serversList)) {
|
if (serversList.length === 0) {
|
||||||
return (
|
return (
|
||||||
<DropdownItem tag={Link} to="/server/create">
|
<DropdownItem tag={Link} to="/server/create">
|
||||||
<FontAwesomeIcon icon={plusIcon} /> <span className="ms-1">Add a server</span>
|
<FontAwesomeIcon icon={plusIcon} /> <span className="ms-1">Add a server</span>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { omit } from 'ramda';
|
|
||||||
import type { SemVer } from '../../utils/helpers/version';
|
import type { SemVer } from '../../utils/helpers/version';
|
||||||
|
|
||||||
export interface ServerData {
|
export interface ServerData {
|
||||||
|
@ -45,5 +44,4 @@ export const isNotFoundServer = (server: SelectedServer): server is NotFoundServ
|
||||||
|
|
||||||
export const getServerId = (server: SelectedServer) => (isServerWithId(server) ? server.id : '');
|
export const getServerId = (server: SelectedServer) => (isServerWithId(server) ? server.id : '');
|
||||||
|
|
||||||
export const serverWithIdToServerData = (server: ServerWithId): ServerData =>
|
export const serverWithIdToServerData = ({ id, autoConnect, ...server }: ServerWithId): ServerData => server;
|
||||||
omit<ServerWithId, 'id' | 'autoConnect'>(['id', 'autoConnect'], server);
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { complement } from 'ramda';
|
|
||||||
import type { ChangeEvent, PropsWithChildren } from 'react';
|
import type { ChangeEvent, PropsWithChildren } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||||
|
@ -27,8 +26,8 @@ type ImportServersBtnDeps = {
|
||||||
ServersImporter: ServersImporter
|
ServersImporter: ServersImporter
|
||||||
};
|
};
|
||||||
|
|
||||||
const serversFiltering = (servers: ServerData[]) =>
|
const serversInclude = (servers: ServerData[], { url, apiKey }: ServerData) =>
|
||||||
({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey);
|
servers.some((server) => server.url === url && server.apiKey === apiKey);
|
||||||
|
|
||||||
const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBtnDeps> = ({
|
const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBtnDeps> = ({
|
||||||
createServers,
|
createServers,
|
||||||
|
@ -56,7 +55,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||||
serversToCreate.current = newServers;
|
serversToCreate.current = newServers;
|
||||||
|
|
||||||
const existingServers = Object.values(servers);
|
const existingServers = Object.values(servers);
|
||||||
const dupServers = newServers.filter(serversFiltering(existingServers));
|
const dupServers = newServers.filter((server) => serversInclude(existingServers, server));
|
||||||
const hasDuplicatedServers = !!dupServers.length;
|
const hasDuplicatedServers = !!dupServers.length;
|
||||||
|
|
||||||
!hasDuplicatedServers ? create(newServers) : setDuplicatedServers(dupServers);
|
!hasDuplicatedServers ? create(newServers) : setDuplicatedServers(dupServers);
|
||||||
|
@ -75,7 +74,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||||
hideModal();
|
hideModal();
|
||||||
}, [create, hideModal, serversToCreate]);
|
}, [create, hideModal, serversToCreate]);
|
||||||
const createNonDuplicatedServers = useCallback(() => {
|
const createNonDuplicatedServers = useCallback(() => {
|
||||||
create(serversToCreate.current.filter(complement(serversFiltering(duplicatedServers))));
|
create(serversToCreate.current.filter((server) => !serversInclude(duplicatedServers, server)));
|
||||||
hideModal();
|
hideModal();
|
||||||
}, [create, duplicatedServers, hideModal]);
|
}, [create, duplicatedServers, hideModal]);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { memoizeWith } from '@shlinkio/data-manipulation';
|
||||||
import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract';
|
import type { ShlinkHealth } from '@shlinkio/shlink-web-component/api-contract';
|
||||||
import { memoizeWith, pipe } from 'ramda';
|
|
||||||
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
|
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
|
||||||
|
@ -12,9 +12,9 @@ export const MIN_FALLBACK_VERSION = '1.0.0';
|
||||||
export const MAX_FALLBACK_VERSION = '999.999.999';
|
export const MAX_FALLBACK_VERSION = '999.999.999';
|
||||||
export const LATEST_VERSION_CONSTRAINT = 'latest';
|
export const LATEST_VERSION_CONSTRAINT = 'latest';
|
||||||
|
|
||||||
const versionToSemVer = pipe(
|
const versionToSemVer = (version: string) => toSemVer(
|
||||||
(version: string) => (version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version),
|
version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
|
||||||
toSemVer(MIN_FALLBACK_VERSION),
|
MIN_FALLBACK_VERSION,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getServerVersion = memoizeWith(
|
const getServerVersion = memoizeWith(
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { assoc, dissoc, fromPairs, map, pipe, reduce, toPairs } from 'ramda';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||||
|
|
||||||
|
@ -17,14 +16,17 @@ interface SetAutoConnect {
|
||||||
const initialState: ServersMap = {};
|
const initialState: ServersMap = {};
|
||||||
|
|
||||||
const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
||||||
if ((server as ServerWithId).id) {
|
if ('id' in server) {
|
||||||
return server as ServerWithId;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
return assoc('id', uuid(), server);
|
return { ...server, id: uuid() };
|
||||||
};
|
};
|
||||||
|
|
||||||
const serversListToMap = reduce<ServerWithId, ServersMap>((acc, server) => assoc(server.id, server, acc), {});
|
const serversListToMap = (servers: ServerWithId[]): ServersMap => servers.reduce<ServersMap>(
|
||||||
|
(acc, server) => ({ ...acc, [server.id]: server }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
export const { actions, reducer } = createSlice({
|
export const { actions, reducer } = createSlice({
|
||||||
name: 'shlink/servers',
|
name: 'shlink/servers',
|
||||||
|
@ -37,11 +39,14 @@ export const { actions, reducer } = createSlice({
|
||||||
reducer: (state, { payload }: PayloadAction<EditServer>) => {
|
reducer: (state, { payload }: PayloadAction<EditServer>) => {
|
||||||
const { serverId, serverData } = payload;
|
const { serverId, serverData } = payload;
|
||||||
return (
|
return (
|
||||||
!state[serverId] ? state : assoc(serverId, { ...state[serverId], ...serverData }, state)
|
!state[serverId] ? state : { ...state, [serverId]: { ...state[serverId], ...serverData } }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deleteServer: (state, { payload }) => dissoc(payload.id, state),
|
deleteServer: (state, { payload }) => {
|
||||||
|
const { [payload.id]: deletedServer, ...rest } = state;
|
||||||
|
return rest;
|
||||||
|
},
|
||||||
setAutoConnect: {
|
setAutoConnect: {
|
||||||
prepare: ({ id: serverId }: ServerWithId, autoConnect: boolean) => ({
|
prepare: ({ id: serverId }: ServerWithId, autoConnect: boolean) => ({
|
||||||
payload: { serverId, autoConnect },
|
payload: { serverId, autoConnect },
|
||||||
|
@ -53,11 +58,11 @@ export const { actions, reducer } = createSlice({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!autoConnect) {
|
if (!autoConnect) {
|
||||||
return assoc(serverId, { ...state[serverId], autoConnect }, state);
|
return { ...state, [serverId]: { ...state[serverId], autoConnect } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromPairs(
|
return Object.fromEntries(
|
||||||
toPairs(state).map(([evaluatedServerId, server]) => [
|
Object.entries(state).map(([evaluatedServerId, server]) => [
|
||||||
evaluatedServerId,
|
evaluatedServerId,
|
||||||
{ ...server, autoConnect: evaluatedServerId === serverId },
|
{ ...server, autoConnect: evaluatedServerId === serverId },
|
||||||
]),
|
]),
|
||||||
|
@ -65,11 +70,10 @@ export const { actions, reducer } = createSlice({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createServers: {
|
createServers: {
|
||||||
prepare: pipe(
|
prepare: (servers: ServerData[]) => {
|
||||||
map(serverWithId),
|
const payload = serversListToMap(servers.map(serverWithId));
|
||||||
serversListToMap,
|
return { payload };
|
||||||
(payload: ServersMap) => ({ payload }),
|
},
|
||||||
),
|
|
||||||
reducer: (state, { payload: newServers }: PayloadAction<ServersMap>) => ({ ...state, ...newServers }),
|
reducer: (state, { payload: newServers }: PayloadAction<ServersMap>) => ({ ...state, ...newServers }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { values } from 'ramda';
|
|
||||||
import type { JsonToCsv } from '../../utils/helpers/csvjson';
|
import type { JsonToCsv } from '../../utils/helpers/csvjson';
|
||||||
import { saveCsv } from '../../utils/helpers/files';
|
import { saveCsv } from '../../utils/helpers/files';
|
||||||
import type { LocalStorage } from '../../utils/services/LocalStorage';
|
import type { LocalStorage } from '../../utils/services/LocalStorage';
|
||||||
|
@ -15,7 +14,7 @@ export class ServersExporter {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public readonly exportServers = async () => {
|
public readonly exportServers = async () => {
|
||||||
const servers = values(this.storage.get<ServersMap>('servers') ?? {}).map(serverWithIdToServerData);
|
const servers = Object.values(this.storage.get<ServersMap>('servers') ?? {}).map(serverWithIdToServerData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const csv = this.jsonToCsv(servers);
|
const csv = this.jsonToCsv(servers);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { prop } from 'ramda';
|
|
||||||
import type { ConnectDecorator } from '../../container/types';
|
import type { ConnectDecorator } from '../../container/types';
|
||||||
import { CreateServerFactory } from '../CreateServer';
|
import { CreateServerFactory } from '../CreateServer';
|
||||||
import { DeleteServerButtonFactory } from '../DeleteServerButton';
|
import { DeleteServerButtonFactory } from '../DeleteServerButton';
|
||||||
|
@ -70,5 +69,5 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
|
bottle.serviceFactory('selectedServerReducerCreator', selectedServerReducerCreator, 'selectServer');
|
||||||
bottle.serviceFactory('selectedServerReducer', prop('reducer'), 'selectedServerReducerCreator');
|
bottle.serviceFactory('selectedServerReducer', (obj) => obj.reducer, 'selectedServerReducerCreator');
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
|
import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { mergeDeepRight } from '@shlinkio/data-manipulation';
|
||||||
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
import type { Theme } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type {
|
import type {
|
||||||
Settings,
|
Settings,
|
||||||
|
@ -8,7 +9,6 @@ import type {
|
||||||
TagsSettings,
|
TagsSettings,
|
||||||
VisitsSettings,
|
VisitsSettings,
|
||||||
} from '@shlinkio/shlink-web-component';
|
} from '@shlinkio/shlink-web-component';
|
||||||
import { mergeDeepRight } from 'ramda';
|
|
||||||
import type { Defined } from '../../utils/types';
|
import type { Defined } from '../../utils/types';
|
||||||
|
|
||||||
type ShortUrlsOrder = Defined<ShortUrlsListSettings['defaultOrdering']>;
|
type ShortUrlsOrder = Defined<ShortUrlsListSettings['defaultOrdering']>;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { AsyncThunkPayloadCreator } from '@reduxjs/toolkit';
|
import type { AsyncThunkPayloadCreator } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk as baseCreateAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk as baseCreateAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { identity } from 'ramda';
|
|
||||||
import type { ShlinkState } from '../../container/types';
|
import type { ShlinkState } from '../../container/types';
|
||||||
|
|
||||||
export const createAsyncThunk = <Returned, ThunkArg>(
|
export const createAsyncThunk = <Returned, ThunkArg>(
|
||||||
|
@ -9,5 +8,5 @@ export const createAsyncThunk = <Returned, ThunkArg>(
|
||||||
) => baseCreateAsyncThunk(
|
) => baseCreateAsyncThunk(
|
||||||
typePrefix,
|
typePrefix,
|
||||||
payloadCreator,
|
payloadCreator,
|
||||||
{ serializeError: identity },
|
{ serializeError: (e) => e },
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import { memoizeWith } from '@shlinkio/data-manipulation';
|
||||||
import { compare } from 'compare-versions';
|
import { compare } from 'compare-versions';
|
||||||
import { identity, isEmpty, isNil, memoizeWith } from 'ramda';
|
|
||||||
|
|
||||||
export type Empty = null | undefined | '' | never[];
|
export type Empty = null | undefined | '' | never[];
|
||||||
|
|
||||||
const hasValue = <T>(value: T | Empty): value is T => !isNil(value) && !isEmpty(value);
|
const isEmpty = (value: Exclude<any, undefined | null>): boolean => (
|
||||||
|
(Array.isArray(value) && value.length === 0)
|
||||||
|
|| (typeof value === 'string' && value === '')
|
||||||
|
|| (typeof value === 'object' && Object.keys(value).length === 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const hasValue = <T>(value: T | Empty): value is T => value !== undefined && value !== null && !isEmpty(value);
|
||||||
|
|
||||||
type SemVerPatternFragment = `${bigint | '*'}`;
|
type SemVerPatternFragment = `${bigint | '*'}`;
|
||||||
|
|
||||||
|
@ -29,7 +35,7 @@ export const versionMatch = (versionToMatch: SemVer | Empty, { maxVersion, minVe
|
||||||
return matchesMaxVersion && matchesMinVersion;
|
return matchesMaxVersion && matchesMinVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
const versionIsValidSemVer = memoizeWith(identity, (version: string): version is SemVer => {
|
const versionIsValidSemVer = memoizeWith((v) => v, (version: string): version is SemVer => {
|
||||||
try {
|
try {
|
||||||
return compare(version, version, '=');
|
return compare(version, version, '=');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -39,5 +45,6 @@ const versionIsValidSemVer = memoizeWith(identity, (version: string): version is
|
||||||
|
|
||||||
export const versionToPrintable = (version: string) => (!versionIsValidSemVer(version) ? version : `v${version}`);
|
export const versionToPrintable = (version: string) => (!versionIsValidSemVer(version) ? version : `v${version}`);
|
||||||
|
|
||||||
export const versionToSemVer = (defaultValue: SemVer = 'latest') =>
|
export const versionToSemVer = (version: string, fallback: SemVer = 'latest'): SemVer => (
|
||||||
(version: string): SemVer => (versionIsValidSemVer(version) ? version : defaultValue);
|
versionIsValidSemVer(version) ? version : fallback
|
||||||
|
);
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { pipe, range } from 'ramda';
|
|
||||||
import type { SyntheticEvent } from 'react';
|
import type { SyntheticEvent } from 'react';
|
||||||
|
|
||||||
type Optional<T> = T | null | undefined;
|
export const handleEventPreventingDefault = <T>(handler: () => T) => (e: SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
export type OptionalString = Optional<string>;
|
handler();
|
||||||
|
};
|
||||||
export const handleEventPreventingDefault = <T>(handler: () => T) => pipe(
|
|
||||||
(e: SyntheticEvent) => e.preventDefault(),
|
|
||||||
handler,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const rangeOf = <T>(size: number, mappingFn: (value: number) => T, startAt = 1): T[] =>
|
|
||||||
range(startAt, size + 1).map(mappingFn);
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { values } from 'ramda';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import type { ServersMap } from '../../src/servers/data';
|
import type { ServersMap } from '../../src/servers/data';
|
||||||
import { ServersDropdown } from '../../src/servers/ServersDropdown';
|
import { ServersDropdown } from '../../src/servers/ServersDropdown';
|
||||||
|
@ -28,7 +27,7 @@ describe('<ServersDropdown />', () => {
|
||||||
|
|
||||||
await user.click(screen.getByText('Servers'));
|
await user.click(screen.getByText('Servers'));
|
||||||
const items = screen.getAllByRole('menuitem');
|
const items = screen.getAllByRole('menuitem');
|
||||||
expect(items).toHaveLength(values(fallbackServers).length + 1);
|
expect(items).toHaveLength(Object.values(fallbackServers).length + 1);
|
||||||
expect(items[0]).toHaveTextContent('foo');
|
expect(items[0]).toHaveTextContent('foo');
|
||||||
expect(items[1]).toHaveTextContent('bar');
|
expect(items[1]).toHaveTextContent('bar');
|
||||||
expect(items[2]).toHaveTextContent('baz');
|
expect(items[2]).toHaveTextContent('baz');
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { dissoc, values } from 'ramda';
|
|
||||||
import type { RegularServer, ServersMap, ServerWithId } from '../../../src/servers/data';
|
import type { RegularServer, ServersMap, ServerWithId } from '../../../src/servers/data';
|
||||||
import {
|
import {
|
||||||
createServers,
|
createServers,
|
||||||
|
@ -101,17 +100,17 @@ describe('serversReducer', () => {
|
||||||
|
|
||||||
describe('createServers', () => {
|
describe('createServers', () => {
|
||||||
it('returns expected action', () => {
|
it('returns expected action', () => {
|
||||||
const newServers = values(list);
|
const newServers = Object.values(list);
|
||||||
const { payload } = createServers(newServers);
|
const { payload } = createServers(newServers);
|
||||||
|
|
||||||
expect(payload).toEqual(list);
|
expect(payload).toEqual(list);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates an id for every provided server if they do not have it', () => {
|
it('generates an id for every provided server if they do not have it', () => {
|
||||||
const servers = values(list).map(dissoc('id'));
|
const servers = Object.values(list).map(({ id, ...rest }) => rest);
|
||||||
const { payload } = createServers(servers);
|
const { payload } = createServers(servers);
|
||||||
|
|
||||||
expect(values(payload).every(({ id }) => !!id)).toEqual(true);
|
expect(Object.values(payload).every(({ id }) => !!id)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue