Merge pull request #950 from acelaya-forks/feature/remove-ramda

Feature/remove ramda
This commit is contained in:
Alejandro Celaya 2023-11-01 16:24:08 +01:00 committed by GitHub
commit 788026f2d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 79 additions and 98 deletions

View file

@ -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
View file

@ -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,

View file

@ -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",

View file

@ -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

View file

@ -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;

View file

@ -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, {}),
); );

View file

@ -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>

View file

@ -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);

View file

@ -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]);

View file

@ -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(

View file

@ -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 }),
}, },
}, },

View file

@ -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);

View file

@ -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');
}; };

View file

@ -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']>;

View file

@ -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 },
); );

View file

@ -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
);

View file

@ -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);

View file

@ -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');

View file

@ -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);
}); });
}); });