mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 01:37:24 +03:00
Merge pull request #722 from acelaya-forks/feature/redux-toolkit-poc
Feature/redux toolkit poc
This commit is contained in:
commit
26c3ea19f4
18 changed files with 325 additions and 234 deletions
|
@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
### Changed
|
||||
* [#713](https://github.com/shlinkio/shlink-web-client/issues/713) Updated dependencies.
|
||||
* [#620](https://github.com/shlinkio/shlink-web-client/issues/620) Migrated domains-related reducers to redux toolkit as a POC.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
|
71
package-lock.json
generated
71
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
"axios": "^1.1.2",
|
||||
"bootstrap": "^5.2.2",
|
||||
"bottlejs": "^2.0.1",
|
||||
|
@ -4267,6 +4268,29 @@
|
|||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.0.tgz",
|
||||
"integrity": "sha512-ak11IrjYcUXRqlhNPwnz6AcvA2ynJTu8PzDbbqQw4a3xR4KZtgiqbNblQD+10CRbfK4+5C79SOyxnT9dhBqFnA==",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.16",
|
||||
"redux": "^4.2.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"reselect": "^4.1.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18",
|
||||
"react-redux": "^7.2.1 || ^8.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.1.tgz",
|
||||
|
@ -12143,10 +12167,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.12",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
|
||||
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
|
||||
"dev": true,
|
||||
"version": "9.0.16",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
|
||||
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
|
@ -22723,9 +22746,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
|
||||
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"peerDependencies": {
|
||||
"redux": "^4"
|
||||
}
|
||||
|
@ -22895,6 +22918,11 @@
|
|||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
|
||||
"integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
|
@ -29896,6 +29924,17 @@
|
|||
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.0.tgz",
|
||||
"integrity": "sha512-ak11IrjYcUXRqlhNPwnz6AcvA2ynJTu8PzDbbqQw4a3xR4KZtgiqbNblQD+10CRbfK4+5C79SOyxnT9dhBqFnA==",
|
||||
"requires": {
|
||||
"immer": "^9.0.16",
|
||||
"redux": "^4.2.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"reselect": "^4.1.7"
|
||||
}
|
||||
},
|
||||
"@remix-run/router": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.1.tgz",
|
||||
|
@ -35854,10 +35893,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"immer": {
|
||||
"version": "9.0.12",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
|
||||
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
|
||||
"dev": true
|
||||
"version": "9.0.16",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.16.tgz",
|
||||
"integrity": "sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ=="
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.0.0",
|
||||
|
@ -43653,9 +43691,9 @@
|
|||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
|
||||
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"regenerate": {
|
||||
|
@ -43789,6 +43827,11 @@
|
|||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
|
||||
"integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
"axios": "^1.1.2",
|
||||
"bootstrap": "^5.2.2",
|
||||
"bottlejs": "^2.0.1",
|
||||
|
|
|
@ -8,7 +8,11 @@ import {
|
|||
RegularNotFound,
|
||||
} from '../types/errors';
|
||||
|
||||
export const parseApiError = (e: AxiosError<ProblemDetailsError>) => e.response?.data;
|
||||
const isAxiosError = (e: unknown): e is AxiosError<ProblemDetailsError> => !!e && typeof e === 'object' && 'response' in e;
|
||||
|
||||
export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (
|
||||
isAxiosError(e) ? e.response?.data : undefined
|
||||
);
|
||||
|
||||
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
|
||||
error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT;
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import ReduxThunk from 'redux-thunk';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import { IContainer } from 'bottlejs';
|
||||
import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
||||
import reducers from '../reducers';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import reducer from '../reducers';
|
||||
import { migrateDeprecatedSettings } from '../settings/helpers';
|
||||
import { ShlinkState } from './types';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const localStorageConfig: RLSOptions = {
|
||||
states: ['settings', 'servers'],
|
||||
namespace: 'shlink',
|
||||
|
@ -17,6 +14,11 @@ const localStorageConfig: RLSOptions = {
|
|||
};
|
||||
const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState);
|
||||
|
||||
export const store = createStore(reducers, preloadedState, composeEnhancers(
|
||||
applyMiddleware(save(localStorageConfig), ReduxThunk),
|
||||
));
|
||||
export const setUpStore = (container: IContainer) => configureStore({
|
||||
devTools: !isProduction,
|
||||
reducer: reducer(container),
|
||||
preloadedState,
|
||||
middleware: (defaultMiddlewaresIncludingReduxThunk) => defaultMiddlewaresIncludingReduxThunk(
|
||||
{ immutableCheck: false, serializableCheck: false }, // State is too big for these
|
||||
).concat(save(localStorageConfig)),
|
||||
});
|
||||
|
|
|
@ -8,11 +8,12 @@ import { SelectedServer } from '../servers/data';
|
|||
import { Domain } from './data';
|
||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||
import { DomainDropdown } from './helpers/DomainDropdown';
|
||||
import { EditDomainRedirects } from './reducers/domainRedirects';
|
||||
|
||||
interface DomainRowProps {
|
||||
domain: Domain;
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => void;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Result } from '../utils/Result';
|
|||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { SearchField } from '../utils/SearchField';
|
||||
import { ShlinkDomainRedirects } from '../api/types';
|
||||
import { EditDomainRedirects } from './reducers/domainRedirects';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { DomainsList } from './reducers/domainsList';
|
||||
import { DomainRow } from './DomainRow';
|
||||
|
@ -12,7 +12,7 @@ import { DomainRow } from './DomainRow';
|
|||
interface ManageDomainsProps {
|
||||
listDomains: Function;
|
||||
filterDomains: (searchTerm: string) => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => void;
|
||||
domainsList: DomainsList;
|
||||
selectedServer: SelectedServer;
|
||||
|
|
|
@ -7,14 +7,14 @@ import { useToggle } from '../../utils/helpers/hooks';
|
|||
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
||||
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
||||
import { Domain } from '../data';
|
||||
import { ShlinkDomainRedirects } from '../../api/types';
|
||||
import { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
|
||||
import { getServerId, SelectedServer } from '../../servers/data';
|
||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||
|
||||
interface DomainDropdownProps {
|
||||
domain: Domain;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
||||
import { ShlinkDomain } from '../../api/types';
|
||||
import { InputFormGroup, InputFormGroupProps } from '../../utils/forms/InputFormGroup';
|
||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
||||
import { InfoTooltip } from '../../utils/InfoTooltip';
|
||||
import { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||
|
||||
interface EditDomainRedirectsModalProps {
|
||||
domain: ShlinkDomain;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
}
|
||||
|
||||
const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
|
||||
|
@ -30,10 +31,13 @@ export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
|||
const [invalidShortUrlRedirect, setInvalidShortUrlRedirect] = useState(
|
||||
domain.redirects?.invalidShortUrlRedirect ?? '',
|
||||
);
|
||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
|
||||
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
|
||||
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
|
||||
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
|
||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects({
|
||||
domain: domain.domain,
|
||||
redirects: {
|
||||
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
|
||||
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
|
||||
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
|
||||
},
|
||||
}).then(toggle));
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,31 +1,23 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { ShlinkDomainRedirects } from '../../api/types';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ShlinkState } from '../../container/types';
|
||||
|
||||
export const EDIT_DOMAIN_REDIRECTS_START = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_START';
|
||||
export const EDIT_DOMAIN_REDIRECTS_ERROR = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS_ERROR';
|
||||
export const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||
|
||||
export interface EditDomainRedirectsAction extends Action<string> {
|
||||
export interface EditDomainRedirects {
|
||||
domain: string;
|
||||
redirects: ShlinkDomainRedirects;
|
||||
}
|
||||
|
||||
export const editDomainRedirects = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||
domain: string,
|
||||
domainRedirects: Partial<ShlinkDomainRedirects>,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({ type: EDIT_DOMAIN_REDIRECTS_START });
|
||||
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
export const editDomainRedirects = (
|
||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||
) => createAsyncThunk<EditDomainRedirects, EditDomainRedirects, { state: ShlinkState }>(
|
||||
EDIT_DOMAIN_REDIRECTS,
|
||||
async ({ domain, redirects: domainRedirects }, { getState }) => {
|
||||
const { editDomainRedirects: shlinkEditDomainRedirects } = buildShlinkApiClient(getState);
|
||||
const redirects = await shlinkEditDomainRedirects({ domain, ...domainRedirects });
|
||||
|
||||
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
||||
} catch (e: any) {
|
||||
dispatch<ApiErrorAction>({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
return { domain, redirects };
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { createSlice, createAsyncThunk, createAction, SliceCaseReducers, AsyncThunk } from '@reduxjs/toolkit';
|
||||
import { ShlinkDomainRedirects } from '../../api/types';
|
||||
import { buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { ShlinkState } from '../../container/types';
|
||||
import { Domain, DomainStatus } from '../data';
|
||||
import { hasServerData } from '../../servers/data';
|
||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||
import { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
||||
import { ProblemDetailsError } from '../../api/types/errors';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { EditDomainRedirects } from './domainRedirects';
|
||||
|
||||
export const LIST_DOMAINS_START = 'shlink/domainsList/LIST_DOMAINS_START';
|
||||
export const LIST_DOMAINS_ERROR = 'shlink/domainsList/LIST_DOMAINS_ERROR';
|
||||
export const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
||||
export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
||||
export const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
||||
const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
||||
const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
||||
const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
||||
|
||||
export interface DomainsList {
|
||||
domains: Domain[];
|
||||
|
@ -26,16 +22,12 @@ export interface DomainsList {
|
|||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export interface ListDomainsAction extends Action<string> {
|
||||
interface ListDomains {
|
||||
domains: Domain[];
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
}
|
||||
|
||||
interface FilterDomainsAction extends Action<string> {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
interface ValidateDomain extends Action<string> {
|
||||
interface ValidateDomain {
|
||||
domain: string;
|
||||
status: DomainStatus;
|
||||
}
|
||||
|
@ -47,83 +39,92 @@ const initialState: DomainsList = {
|
|||
error: false,
|
||||
};
|
||||
|
||||
export type DomainsCombinedAction = ListDomainsAction
|
||||
& ApiErrorAction
|
||||
& FilterDomainsAction
|
||||
& EditDomainRedirectsAction
|
||||
& ValidateDomain;
|
||||
|
||||
export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
||||
export const replaceRedirectsOnDomain = ({ domain, redirects }: EditDomainRedirects) =>
|
||||
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, redirects });
|
||||
|
||||
export const replaceStatusOnDomain = (domain: string, status: DomainStatus) =>
|
||||
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
|
||||
|
||||
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
||||
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
||||
[LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }),
|
||||
[LIST_DOMAINS]: (_, { domains, defaultRedirects }) =>
|
||||
({ ...initialState, domains, filteredDomains: domains, defaultRedirects }),
|
||||
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
|
||||
...state,
|
||||
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm.toLowerCase())),
|
||||
}),
|
||||
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
|
||||
...state,
|
||||
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
}),
|
||||
[VALIDATE_DOMAIN]: (state, { domain, status }) => ({
|
||||
...state,
|
||||
domains: state.domains.map(replaceStatusOnDomain(domain, status)),
|
||||
filteredDomains: state.filteredDomains.map(replaceStatusOnDomain(domain, status)),
|
||||
}),
|
||||
}, initialState);
|
||||
|
||||
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
||||
dispatch: Dispatch,
|
||||
getState: GetState,
|
||||
export const domainsListReducerCreator = (
|
||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
||||
) => {
|
||||
dispatch({ type: LIST_DOMAINS_START });
|
||||
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
|
||||
const listDomains = createAsyncThunk<ListDomains, void, { state: ShlinkState }>(
|
||||
LIST_DOMAINS,
|
||||
async (_, { getState }) => {
|
||||
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
|
||||
const { data, defaultRedirects } = await shlinkListDomains();
|
||||
|
||||
try {
|
||||
const resp = await shlinkListDomains().then(({ data, defaultRedirects }) => ({
|
||||
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||
defaultRedirects,
|
||||
}));
|
||||
return {
|
||||
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||
defaultRedirects,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, ...resp });
|
||||
} catch (e: any) {
|
||||
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
||||
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
|
||||
|
||||
export const checkDomainHealth = (buildShlinkApiClient: ShlinkApiClientBuilder) => (domain: string) => async (
|
||||
dispatch: Dispatch,
|
||||
getState: GetState,
|
||||
) => {
|
||||
const { selectedServer } = getState();
|
||||
|
||||
if (!hasServerData(selectedServer)) {
|
||||
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { url, ...rest } = selectedServer;
|
||||
const { health } = buildShlinkApiClient({
|
||||
...rest,
|
||||
url: replaceAuthorityFromUri(url, domain),
|
||||
});
|
||||
|
||||
const { status } = await health();
|
||||
|
||||
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: status === 'pass' ? 'valid' : 'invalid' });
|
||||
} catch (e) {
|
||||
dispatch<ValidateDomain>({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||
}
|
||||
const checkDomainHealth = createAsyncThunk<ValidateDomain, string, { state: ShlinkState }>(
|
||||
VALIDATE_DOMAIN,
|
||||
async (domain: string, { getState }) => {
|
||||
const { selectedServer } = getState();
|
||||
|
||||
if (!hasServerData(selectedServer)) {
|
||||
return { domain, status: 'invalid' };
|
||||
}
|
||||
|
||||
try {
|
||||
const { url, ...rest } = selectedServer;
|
||||
const { health } = buildShlinkApiClient({
|
||||
...rest,
|
||||
url: replaceAuthorityFromUri(url, domain),
|
||||
});
|
||||
|
||||
const { status } = await health();
|
||||
|
||||
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
|
||||
} catch (e) {
|
||||
return { domain, status: 'invalid' };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const filterDomains = createAction<string>(FILTER_DOMAINS);
|
||||
|
||||
const { reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
|
||||
name: 'domainsList',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listDomains.pending, () => ({ ...initialState, loading: true }));
|
||||
builder.addCase(listDomains.rejected, (_, { error }) => (
|
||||
{ ...initialState, error: true, errorData: parseApiError(error) }
|
||||
));
|
||||
builder.addCase(listDomains.fulfilled, (_, { payload }) => (
|
||||
{ ...initialState, ...payload, filteredDomains: payload.domains }
|
||||
));
|
||||
|
||||
builder.addCase(checkDomainHealth.fulfilled, ({ domains, filteredDomains, ...rest }, { payload }) => ({
|
||||
...rest,
|
||||
domains: domains.map(replaceStatusOnDomain(payload.domain, payload.status)),
|
||||
filteredDomains: filteredDomains.map(replaceStatusOnDomain(payload.domain, payload.status)),
|
||||
}));
|
||||
|
||||
builder.addCase(filterDomains, (state, { payload }) => ({
|
||||
...state,
|
||||
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(payload.toLowerCase())),
|
||||
}));
|
||||
|
||||
builder.addCase(editDomainRedirects.fulfilled, (state, { payload }) => ({
|
||||
...state,
|
||||
domains: state.domains.map(replaceRedirectsOnDomain(payload)),
|
||||
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(payload)),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
reducer,
|
||||
listDomains,
|
||||
checkDomainHealth,
|
||||
filterDomains,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { prop } from 'ramda';
|
||||
import Bottle from 'bottlejs';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { checkDomainHealth, filterDomains, listDomains } from '../reducers/domainsList';
|
||||
import { domainsListReducerCreator } from '../reducers/domainsList';
|
||||
import { DomainSelector } from '../DomainSelector';
|
||||
import { ManageDomains } from '../ManageDomains';
|
||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||
|
@ -16,11 +17,20 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
||||
));
|
||||
|
||||
// Reducer
|
||||
bottle.serviceFactory(
|
||||
'domainsListReducerCreator',
|
||||
domainsListReducerCreator,
|
||||
'buildShlinkApiClient',
|
||||
'editDomainRedirects',
|
||||
);
|
||||
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('filterDomains', () => filterDomains);
|
||||
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
|
||||
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
|
||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('checkDomainHealth', checkDomainHealth, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
|
|||
import { BrowserRouter } from 'react-router-dom';
|
||||
import pack from '../package.json';
|
||||
import { container } from './container';
|
||||
import { store } from './container/store';
|
||||
import { setUpStore } from './container/store';
|
||||
import { fixLeafletIcons } from './utils/helpers/leaflet';
|
||||
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
||||
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
|
||||
|
@ -14,6 +14,7 @@ import './index.scss';
|
|||
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
|
||||
fixLeafletIcons();
|
||||
|
||||
const store = setUpStore(container);
|
||||
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;
|
||||
|
||||
createRoot(document.getElementById('root')!).render( // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { IContainer } from 'bottlejs';
|
||||
import { combineReducers } from 'redux';
|
||||
import serversReducer from '../servers/reducers/servers';
|
||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||
|
@ -16,13 +17,12 @@ import tagDeleteReducer from '../tags/reducers/tagDelete';
|
|||
import tagEditReducer from '../tags/reducers/tagEdit';
|
||||
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
||||
import settingsReducer from '../settings/reducers/settings';
|
||||
import domainsListReducer from '../domains/reducers/domainsList';
|
||||
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
|
||||
import appUpdatesReducer from '../app/reducers/appUpdates';
|
||||
import sidebarReducer from '../common/reducers/sidebar';
|
||||
import { ShlinkState } from '../container/types';
|
||||
|
||||
export default combineReducers<ShlinkState>({
|
||||
export default (container: IContainer) => combineReducers<ShlinkState>({
|
||||
servers: serversReducer,
|
||||
selectedServer: selectedServerReducer,
|
||||
shortUrlsList: shortUrlsListReducer,
|
||||
|
@ -40,7 +40,7 @@ export default combineReducers<ShlinkState>({
|
|||
tagEdit: tagEditReducer,
|
||||
mercureInfo: mercureInfoReducer,
|
||||
settings: settingsReducer,
|
||||
domainsList: domainsListReducer,
|
||||
domainsList: container.domainsListReducer,
|
||||
visitsOverview: visitsOverviewReducer,
|
||||
appUpdated: appUpdatesReducer,
|
||||
sidebar: sidebarReducer,
|
||||
|
|
|
@ -71,7 +71,7 @@ describe('<DomainDropdown />', () => {
|
|||
|
||||
expect(editDomainRedirects).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByText('Save'));
|
||||
expect(editDomainRedirects).toHaveBeenCalledWith(domain, expect.anything());
|
||||
expect(editDomainRedirects).toHaveBeenCalledWith(expect.objectContaining({ domain }));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
});
|
||||
|
|
|
@ -40,37 +40,49 @@ describe('<EditDomainRedirectsModal />', () => {
|
|||
|
||||
expect(editDomainRedirects).not.toHaveBeenCalled();
|
||||
submitForm();
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', {
|
||||
baseUrlRedirect: 'baz',
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: null,
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({
|
||||
domain: 'foo.com',
|
||||
redirects: {
|
||||
baseUrlRedirect: 'baz',
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: null,
|
||||
},
|
||||
}));
|
||||
|
||||
await user.clear(screen.getByDisplayValue('baz'));
|
||||
await user.type(screen.getAllByPlaceholderText('No redirect')[0], 'new_base_url');
|
||||
await user.type(screen.getAllByPlaceholderText('No redirect')[2], 'new_invalid_short_url');
|
||||
submitForm();
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', {
|
||||
baseUrlRedirect: 'new_base_url',
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: 'new_invalid_short_url',
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({
|
||||
domain: 'foo.com',
|
||||
redirects: {
|
||||
baseUrlRedirect: 'new_base_url',
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: 'new_invalid_short_url',
|
||||
},
|
||||
}));
|
||||
|
||||
await user.type(screen.getAllByPlaceholderText('No redirect')[1], 'new_regular_404');
|
||||
await user.clear(screen.getByDisplayValue('new_invalid_short_url'));
|
||||
submitForm();
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', {
|
||||
baseUrlRedirect: 'new_base_url',
|
||||
regular404Redirect: 'new_regular_404',
|
||||
invalidShortUrlRedirect: null,
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({
|
||||
domain: 'foo.com',
|
||||
redirects: {
|
||||
baseUrlRedirect: 'new_base_url',
|
||||
regular404Redirect: 'new_regular_404',
|
||||
invalidShortUrlRedirect: null,
|
||||
},
|
||||
}));
|
||||
|
||||
await Promise.all(screen.getAllByPlaceholderText('No redirect').map((element) => user.clear(element)));
|
||||
submitForm();
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith('foo.com', {
|
||||
baseUrlRedirect: null,
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: null,
|
||||
await waitFor(() => expect(editDomainRedirects).toHaveBeenCalledWith({
|
||||
domain: 'foo.com',
|
||||
redirects: {
|
||||
baseUrlRedirect: null,
|
||||
regular404Redirect: null,
|
||||
invalidShortUrlRedirect: null,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { Mock } from 'ts-mockery';
|
||||
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
||||
import {
|
||||
EDIT_DOMAIN_REDIRECTS,
|
||||
EDIT_DOMAIN_REDIRECTS_ERROR,
|
||||
EDIT_DOMAIN_REDIRECTS_START,
|
||||
editDomainRedirects as editDomainRedirectsAction,
|
||||
} from '../../../src/domains/reducers/domainRedirects';
|
||||
import { EditDomainRedirects, editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
|
||||
import { ShlinkDomainRedirects } from '../../../src/api/types';
|
||||
|
||||
describe('domainRedirectsReducer', () => {
|
||||
|
@ -16,29 +11,33 @@ describe('domainRedirectsReducer', () => {
|
|||
const redirects = Mock.all<ShlinkDomainRedirects>();
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const editDomainRedirects = jest.fn();
|
||||
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ editDomainRedirects });
|
||||
const editDomainRedirectsCall = jest.fn();
|
||||
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall });
|
||||
const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient);
|
||||
|
||||
it('dispatches error when loading domains fails', async () => {
|
||||
editDomainRedirects.mockRejectedValue(new Error('error'));
|
||||
editDomainRedirectsCall.mockRejectedValue(new Error('error'));
|
||||
|
||||
await editDomainRedirectsAction(buildShlinkApiClient)(domain, {})(dispatch, getState);
|
||||
await editDomainRedirectsAction(Mock.of<EditDomainRedirects>({ domain }))(dispatch, getState, {});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS_ERROR });
|
||||
expect(editDomainRedirects).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: editDomainRedirectsAction.rejected.toString(),
|
||||
}));
|
||||
expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dispatches domain and redirects once loaded', async () => {
|
||||
editDomainRedirects.mockResolvedValue(redirects);
|
||||
editDomainRedirectsCall.mockResolvedValue(redirects);
|
||||
|
||||
await editDomainRedirectsAction(buildShlinkApiClient)(domain, {})(dispatch, getState);
|
||||
await editDomainRedirectsAction(Mock.of<EditDomainRedirects>({ domain }))(dispatch, getState, {});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: EDIT_DOMAIN_REDIRECTS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
||||
expect(editDomainRedirects).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: editDomainRedirectsAction.fulfilled.toString(),
|
||||
payload: { domain, redirects },
|
||||
}));
|
||||
expect(editDomainRedirectsCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
import { Mock } from 'ts-mockery';
|
||||
import reducer, {
|
||||
LIST_DOMAINS,
|
||||
LIST_DOMAINS_ERROR,
|
||||
LIST_DOMAINS_START,
|
||||
FILTER_DOMAINS,
|
||||
VALIDATE_DOMAIN,
|
||||
DomainsCombinedAction,
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
DomainsList,
|
||||
listDomains as listDomainsAction,
|
||||
filterDomains as filterDomainsAction,
|
||||
replaceRedirectsOnDomain,
|
||||
checkDomainHealth,
|
||||
replaceStatusOnDomain,
|
||||
domainsListReducerCreator,
|
||||
} from '../../../src/domains/reducers/domainsList';
|
||||
import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects';
|
||||
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
|
||||
import { ShlinkDomainRedirects } from '../../../src/api/types';
|
||||
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
||||
import { Domain } from '../../../src/domains/data';
|
||||
import { ShlinkState } from '../../../src/container/types';
|
||||
import { SelectedServer, ServerData } from '../../../src/servers/data';
|
||||
import { parseApiError } from '../../../src/api/utils';
|
||||
|
||||
describe('domainsListReducer', () => {
|
||||
const dispatch = jest.fn();
|
||||
|
@ -31,36 +25,42 @@ describe('domainsListReducer', () => {
|
|||
Mock.of<Domain>({ domain: 'Boo', status: 'validating' }),
|
||||
];
|
||||
const domains = [...filteredDomains, Mock.of<Domain>({ domain: 'bar', status: 'validating' })];
|
||||
const error = Mock.of<AxiosError>({
|
||||
response: {
|
||||
data: { type: 'NOT_FOUND', status: 404 },
|
||||
},
|
||||
});
|
||||
const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient);
|
||||
const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator(
|
||||
buildShlinkApiClient,
|
||||
editDomainRedirectsThunk,
|
||||
);
|
||||
|
||||
beforeEach(jest.clearAllMocks);
|
||||
|
||||
describe('reducer', () => {
|
||||
const action = (type: string, args: Partial<DomainsCombinedAction> = {}) => Mock.of<DomainsCombinedAction>(
|
||||
{ type, ...args },
|
||||
);
|
||||
|
||||
it('returns loading on LIST_DOMAINS_START', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_START))).toEqual(
|
||||
expect(reducer(undefined, { type: listDomainsAction.pending.toString() })).toEqual(
|
||||
{ domains: [], filteredDomains: [], loading: true, error: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error on LIST_DOMAINS_ERROR', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_ERROR))).toEqual(
|
||||
{ domains: [], filteredDomains: [], loading: false, error: true },
|
||||
expect(reducer(undefined, { type: listDomainsAction.rejected.toString(), error })).toEqual(
|
||||
{ domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error) },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns domains on LIST_DOMAINS', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS, { domains }))).toEqual(
|
||||
{ domains, filteredDomains: domains, loading: false, error: false },
|
||||
);
|
||||
expect(
|
||||
reducer(undefined, { type: listDomainsAction.fulfilled.toString(), payload: { domains } }),
|
||||
).toEqual({ domains, filteredDomains: domains, loading: false, error: false });
|
||||
});
|
||||
|
||||
it('filters domains on FILTER_DOMAINS', () => {
|
||||
expect(reducer(Mock.of<DomainsList>({ domains }), action(FILTER_DOMAINS, { searchTerm: 'oO' }))).toEqual(
|
||||
{ domains, filteredDomains },
|
||||
);
|
||||
expect(
|
||||
reducer(Mock.of<DomainsList>({ domains }), { type: filterDomains.toString(), payload: 'oO' }),
|
||||
).toEqual({ domains, filteredDomains });
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
@ -74,12 +74,12 @@ describe('domainsListReducer', () => {
|
|||
invalidShortUrlRedirect: null,
|
||||
};
|
||||
|
||||
expect(reducer(
|
||||
Mock.of<DomainsList>({ domains, filteredDomains }),
|
||||
action(EDIT_DOMAIN_REDIRECTS, { domain, redirects }),
|
||||
)).toEqual({
|
||||
domains: domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
expect(reducer(Mock.of<DomainsList>({ domains, filteredDomains }), {
|
||||
type: editDomainRedirectsThunk.fulfilled.toString(),
|
||||
payload: { domain, redirects },
|
||||
})).toEqual({
|
||||
domains: domains.map(replaceRedirectsOnDomain({ domain, redirects })),
|
||||
filteredDomains: filteredDomains.map(replaceRedirectsOnDomain({ domain, redirects })),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -90,7 +90,10 @@ describe('domainsListReducer', () => {
|
|||
])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => {
|
||||
expect(reducer(
|
||||
Mock.of<DomainsList>({ domains, filteredDomains }),
|
||||
action(VALIDATE_DOMAIN, { domain, status: 'valid' }),
|
||||
{
|
||||
type: checkDomainHealth.fulfilled.toString(),
|
||||
payload: { domain, status: 'valid' },
|
||||
},
|
||||
)).toEqual({
|
||||
domains: domains.map(replaceStatusOnDomain(domain, 'valid')),
|
||||
filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')),
|
||||
|
@ -102,22 +105,31 @@ describe('domainsListReducer', () => {
|
|||
it('dispatches error when loading domains fails', async () => {
|
||||
listDomains.mockRejectedValue(new Error('error'));
|
||||
|
||||
await listDomainsAction(buildShlinkApiClient)()(dispatch, getState);
|
||||
await listDomainsAction()(dispatch, getState, {});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS_ERROR });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
type: listDomainsAction.pending.toString(),
|
||||
}));
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
type: listDomainsAction.rejected.toString(),
|
||||
}));
|
||||
expect(listDomains).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dispatches domains once loaded', async () => {
|
||||
listDomains.mockResolvedValue({ data: domains });
|
||||
|
||||
await listDomainsAction(buildShlinkApiClient)()(dispatch, getState);
|
||||
await listDomainsAction()(dispatch, getState, {});
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS, domains, defaultRedirects: undefined });
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
type: listDomainsAction.pending.toString(),
|
||||
}));
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
type: listDomainsAction.fulfilled.toString(),
|
||||
payload: { domains },
|
||||
}));
|
||||
expect(listDomains).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -128,7 +140,9 @@ describe('domainsListReducer', () => {
|
|||
['bar'],
|
||||
['something'],
|
||||
])('creates action as expected', (searchTerm) => {
|
||||
expect(filterDomainsAction(searchTerm)).toEqual({ type: FILTER_DOMAINS, searchTerm });
|
||||
expect(filterDomains(searchTerm)).toEqual(
|
||||
expect.objectContaining({ type: filterDomains.toString(), payload: searchTerm }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -140,12 +154,14 @@ describe('domainsListReducer', () => {
|
|||
selectedServer: Mock.all<SelectedServer>(),
|
||||
}));
|
||||
|
||||
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||
await checkDomainHealth(domain)(dispatch, getState, {});
|
||||
|
||||
expect(getState).toHaveBeenCalledTimes(1);
|
||||
expect(health).not.toHaveBeenCalled();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: checkDomainHealth.fulfilled.toString(),
|
||||
payload: { domain, status: 'invalid' },
|
||||
}));
|
||||
});
|
||||
|
||||
it('dispatches invalid status when health endpoint returns an error', async () => {
|
||||
|
@ -157,12 +173,14 @@ describe('domainsListReducer', () => {
|
|||
}));
|
||||
health.mockRejectedValue({});
|
||||
|
||||
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||
await checkDomainHealth(domain)(dispatch, getState, {});
|
||||
|
||||
expect(getState).toHaveBeenCalledTimes(1);
|
||||
expect(health).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: 'invalid' });
|
||||
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: checkDomainHealth.fulfilled.toString(),
|
||||
payload: { domain, status: 'invalid' },
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
@ -180,12 +198,14 @@ describe('domainsListReducer', () => {
|
|||
}));
|
||||
health.mockResolvedValue({ status: healthStatus });
|
||||
|
||||
await checkDomainHealth(buildShlinkApiClient)(domain)(dispatch, getState);
|
||||
await checkDomainHealth(domain)(dispatch, getState, {});
|
||||
|
||||
expect(getState).toHaveBeenCalledTimes(1);
|
||||
expect(health).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith({ type: VALIDATE_DOMAIN, domain, status: expectedStatus });
|
||||
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
type: checkDomainHealth.fulfilled.toString(),
|
||||
payload: { domain, status: expectedStatus },
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue