mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Implemented logic to edit domain redirects
This commit is contained in:
parent
bf29158a8a
commit
69cb3bd619
28 changed files with 347 additions and 141 deletions
|
@ -16,6 +16,8 @@ import {
|
|||
ShlinkDomain,
|
||||
ShlinkDomainsResponse,
|
||||
ShlinkVisitsOverview,
|
||||
ShlinkEditDomainRedirects,
|
||||
ShlinkDomainRedirects,
|
||||
} from '../types';
|
||||
|
||||
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||
|
@ -108,6 +110,11 @@ export default class ShlinkApiClient {
|
|||
public readonly listDomains = async (): Promise<ShlinkDomain[]> =>
|
||||
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data);
|
||||
|
||||
public readonly editDomainRedirects = async (
|
||||
domainRedirects: ShlinkEditDomainRedirects,
|
||||
): Promise<ShlinkDomainRedirects> =>
|
||||
this.performRequest<ShlinkDomainRedirects>('/domains/redirects', 'PATCH', {}, domainRedirects).then(({ data }) => data);
|
||||
|
||||
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => {
|
||||
try {
|
||||
return await this.axios({
|
||||
|
|
6
src/api/types/actions.ts
Normal file
6
src/api/types/actions.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Action } from 'redux';
|
||||
import { ProblemDetailsError } from './index';
|
||||
|
||||
export interface ApiErrorAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
|
@ -65,10 +65,14 @@ export interface ShlinkShortUrlData extends ShortUrlMeta {
|
|||
tags?: string[];
|
||||
}
|
||||
|
||||
interface ShlinkDomainRedirects {
|
||||
baseUrlRedirect: string,
|
||||
regular404Redirect: string,
|
||||
invalidShortUrlRedirect: string
|
||||
export interface ShlinkDomainRedirects {
|
||||
baseUrlRedirect: string | null;
|
||||
regular404Redirect: string | null;
|
||||
invalidShortUrlRedirect: string | null;
|
||||
}
|
||||
|
||||
export interface ShlinkEditDomainRedirects extends Partial<ShlinkDomainRedirects> {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface ShlinkDomain {
|
||||
|
|
67
src/domains/DomainRow.tsx
Normal file
67
src/domains/DomainRow.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { FC } from 'react';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBan as forbiddenIcon,
|
||||
faCheck as defaultDomainIcon,
|
||||
faEdit as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../api/types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { OptionalString } from '../utils/utils';
|
||||
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
|
||||
|
||||
interface DomainRowProps {
|
||||
domain: ShlinkDomain;
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
}
|
||||
|
||||
const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => (
|
||||
<span className="text-muted">
|
||||
{!fallback && <small>No redirect</small>}
|
||||
{fallback && <>{fallback} <small>(as fallback)</small></>}
|
||||
</span>
|
||||
);
|
||||
const DefaultDomain: FC = () => (
|
||||
<>
|
||||
<FontAwesomeIcon icon={defaultDomainIcon} className="text-primary" id="defaultDomainIcon" />
|
||||
<UncontrolledTooltip target="defaultDomainIcon" placement="right">Default domain</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
export const DomainRow: FC<DomainRowProps> = ({ domain, editDomainRedirects, defaultRedirects }) => {
|
||||
const [ isOpen, toggle ] = useToggle();
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{domain.isDefault ? <DefaultDomain /> : ''}</td>
|
||||
<th>{domain.domain}</th>
|
||||
<td>{domain.redirects?.baseUrlRedirect ?? <Nr fallback={defaultRedirects?.baseUrlRedirect} />}</td>
|
||||
<td>{domain.redirects?.regular404Redirect ?? <Nr fallback={defaultRedirects?.regular404Redirect} />}</td>
|
||||
<td>
|
||||
{domain.redirects?.invalidShortUrlRedirect ?? <Nr fallback={defaultRedirects?.invalidShortUrlRedirect} />}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span id={`domainEdit${domain.domain.replace('.', '')}`}>
|
||||
<Button outline size="sm" disabled={domain.isDefault} onClick={domain.isDefault ? undefined : toggle}>
|
||||
<FontAwesomeIcon icon={domain.isDefault ? forbiddenIcon : editIcon} />
|
||||
</Button>
|
||||
</span>
|
||||
{domain.isDefault && (
|
||||
<UncontrolledTooltip target={`domainEdit${domain.domain.replace('.', '')}`} placement="left">
|
||||
Redirects for default domain cannot be edited here.
|
||||
<br />
|
||||
Use config options or env vars.
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
</td>
|
||||
<EditDomainRedirectsModal
|
||||
domain={domain}
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
};
|
|
@ -1,43 +1,41 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { faCheck as defaultDomainIcon, faEdit as editIcon, faBan as forbiddenIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import Message from '../utils/Message';
|
||||
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 { DomainsList } from './reducers/domainsList';
|
||||
import { DomainRow } from './DomainRow';
|
||||
|
||||
interface ManageDomainsProps {
|
||||
listDomains: Function;
|
||||
filterDomains: (searchTerm: string) => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
const Na: FC = () => <i><small>N/A</small></i>;
|
||||
const DefaultDomain: FC = () => (
|
||||
<>
|
||||
<FontAwesomeIcon icon={defaultDomainIcon} className="text-primary" id="defaultDomainIcon" />
|
||||
<UncontrolledTooltip target="defaultDomainIcon" placement="right">Default domain</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '' ];
|
||||
|
||||
export const ManageDomains: FC<ManageDomainsProps> = ({ listDomains, domainsList }) => {
|
||||
const { domains, loading, error } = domainsList;
|
||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||
{ listDomains, domainsList, filterDomains, editDomainRedirects },
|
||||
) => {
|
||||
const { filteredDomains: domains, loading, error, errorData } = domainsList;
|
||||
const defaultRedirects = domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||
|
||||
useEffect(() => {
|
||||
listDomains();
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Result type="error">
|
||||
<ShlinkApiError fallbackMessage="Error loading domains :(" />
|
||||
<ShlinkApiError errorData={errorData} fallbackMessage="Error loading domains :(" />
|
||||
</Result>
|
||||
);
|
||||
}
|
||||
|
@ -46,36 +44,17 @@ export const ManageDomains: FC<ManageDomainsProps> = ({ listDomains, domainsList
|
|||
<SimpleCard>
|
||||
<table className="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>Domain</th>
|
||||
<th>Base path redirect</th>
|
||||
<th>Regular 404 redirect</th>
|
||||
<th>Invalid short URL redirect</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tr>{headers.map((column, index) => <th key={index}>{column}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domains.length < 1 && <tr><td colSpan={headers.length} className="text-center">No results found</td></tr>}
|
||||
{domains.map((domain) => (
|
||||
<tr key={domain.domain}>
|
||||
<td>{domain.isDefault ? <DefaultDomain /> : ''}</td>
|
||||
<th>{domain.domain}</th>
|
||||
<td>{domain.redirects?.baseUrlRedirect ?? <Na />}</td>
|
||||
<td>{domain.redirects?.regular404Redirect ?? <Na />}</td>
|
||||
<td>{domain.redirects?.invalidShortUrlRedirect ?? <Na />}</td>
|
||||
<td className="text-right">
|
||||
<span id={`domainEdit${domain.domain.replace('.', '')}`}>
|
||||
<Button outline size="sm" disabled={domain.isDefault}>
|
||||
<FontAwesomeIcon icon={domain.isDefault ? forbiddenIcon : editIcon} />
|
||||
</Button>
|
||||
</span>
|
||||
{domain.isDefault && (
|
||||
<UncontrolledTooltip target={`domainEdit${domain.domain.replace('.', '')}`} placement="left">
|
||||
Redirects for default domain cannot be edited here.
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<DomainRow
|
||||
key={domain.domain}
|
||||
domain={domain}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
defaultRedirects={defaultRedirects}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -85,7 +64,7 @@ export const ManageDomains: FC<ManageDomainsProps> = ({ listDomains, domainsList
|
|||
|
||||
return (
|
||||
<>
|
||||
<SearchField className="mb-3" onChange={() => {}} />
|
||||
<SearchField className="mb-3" onChange={filterDomains} />
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
|
|
85
src/domains/helpers/EditDomainRedirectsModal.tsx
Normal file
85
src/domains/helpers/EditDomainRedirectsModal.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ShlinkDomain, ShlinkDomainRedirects } from '../../api/types';
|
||||
import { FormGroupContainer } from '../../utils/FormGroupContainer';
|
||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../utils/utils';
|
||||
|
||||
interface EditDomainRedirectsModalProps {
|
||||
domain: ShlinkDomain;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
}
|
||||
|
||||
const InfoTooltip: FC<{ id: string }> = ({ id, children }) => (
|
||||
<>
|
||||
<FontAwesomeIcon icon={infoIcon} className="mr-2" id={id} />
|
||||
<UncontrolledTooltip target={id} placement="bottom">{children}</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
const FormGroup: FC<{ value: string; onChange: (newValue: string) => void; isLast?: boolean }> = (
|
||||
{ value, onChange, isLast, children },
|
||||
) => (
|
||||
<FormGroupContainer
|
||||
value={value}
|
||||
required={false}
|
||||
type="url"
|
||||
placeholder="No redirect"
|
||||
className={isLast ? 'mb-0' : ''}
|
||||
onChange={onChange}
|
||||
>
|
||||
{children}
|
||||
</FormGroupContainer>
|
||||
);
|
||||
|
||||
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
||||
{ isOpen, toggle, domain, editDomainRedirects },
|
||||
) => {
|
||||
const [ baseUrlRedirect, setBaseUrlRedirect ] = useState('');
|
||||
const [ regular404Redirect, setRegular404Redirect ] = useState('');
|
||||
const [ invalidShortUrlRedirect, setInvalidShortUrlRedirect ] = useState('');
|
||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects(domain.domain, {
|
||||
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
|
||||
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
|
||||
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
|
||||
}).then(toggle));
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ModalHeader toggle={toggle}>
|
||||
Edit redirects for <b>{domain.domain}</b>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
||||
<InfoTooltip id="baseUrlInfo">
|
||||
Visitors accessing the base url, as in <b>https://{domain.domain}/</b>, will be redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Base URL
|
||||
</FormGroup>
|
||||
<FormGroup value={regular404Redirect} onChange={setRegular404Redirect}>
|
||||
<InfoTooltip id="regularNotFoundInfo">
|
||||
Visitors accessing a url not matching a short URL pattern, as in <b>https://{domain.domain}/???/[...]</b>,
|
||||
will be redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Regular 404
|
||||
</FormGroup>
|
||||
<FormGroup value={invalidShortUrlRedirect} isLast onChange={setInvalidShortUrlRedirect}>
|
||||
<InfoTooltip id="invalidShortUrlInfo">
|
||||
Visitors accessing a url matching a short URL pattern, but not matching an existing short code, will be
|
||||
redirected to this URL.
|
||||
</InfoTooltip>
|
||||
Invalid short URL
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" type="button" onClick={toggle}>Cancel</Button>
|
||||
<Button color="primary">Save</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
33
src/domains/reducers/domainRedirects.ts
Normal file
33
src/domains/reducers/domainRedirects.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
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';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
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';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface EditDomainRedirectsAction extends Action<string> {
|
||||
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 } = buildShlinkApiClient(getState);
|
||||
|
||||
try {
|
||||
const redirects = await editDomainRedirects({ domain, ...domainRedirects });
|
||||
|
||||
dispatch<EditDomainRedirectsAction>({ type: EDIT_DOMAIN_REDIRECTS, domain, redirects });
|
||||
} catch (e) {
|
||||
dispatch<ApiErrorAction>({ type: EDIT_DOMAIN_REDIRECTS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
|
@ -1,35 +1,63 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { ShlinkDomain } from '../../api/types';
|
||||
import { ProblemDetailsError, ShlinkDomain, 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 { EDIT_DOMAIN_REDIRECTS, EditDomainRedirectsAction } from './domainRedirects';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
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';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface DomainsList {
|
||||
domains: ShlinkDomain[];
|
||||
filteredDomains: ShlinkDomain[];
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export interface ListDomainsAction extends Action<string> {
|
||||
domains: ShlinkDomain[];
|
||||
}
|
||||
|
||||
interface FilterDomainsAction extends Action<string> {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
const initialState: DomainsList = {
|
||||
domains: [],
|
||||
filteredDomains: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<DomainsList, ListDomainsAction>({
|
||||
type DomainsCombinedAction = ListDomainsAction
|
||||
& ApiErrorAction
|
||||
& FilterDomainsAction
|
||||
& EditDomainRedirectsAction;
|
||||
|
||||
const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomainRedirects) =>
|
||||
(d: ShlinkDomain): ShlinkDomain => d.domain !== domain ? d : { ...d, redirects };
|
||||
|
||||
export default buildReducer<DomainsList, DomainsCombinedAction>({
|
||||
[LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }),
|
||||
[LIST_DOMAINS_ERROR]: () => ({ ...initialState, error: true }),
|
||||
[LIST_DOMAINS]: (_, { domains }) => ({ ...initialState, domains }),
|
||||
[LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }),
|
||||
[LIST_DOMAINS]: (_, { domains }) => ({ ...initialState, domains, filteredDomains: domains }),
|
||||
[FILTER_DOMAINS]: (state, { searchTerm }) => ({
|
||||
...state,
|
||||
filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)),
|
||||
}),
|
||||
[EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({
|
||||
...state,
|
||||
domains: state.domains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
filteredDomains: state.filteredDomains.map(replaceRedirectsOnDomain(domain, redirects)),
|
||||
}),
|
||||
}, initialState);
|
||||
|
||||
export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
||||
|
@ -44,6 +72,8 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => ()
|
|||
|
||||
dispatch<ListDomainsAction>({ type: LIST_DOMAINS, domains });
|
||||
} catch (e) {
|
||||
dispatch({ type: LIST_DOMAINS_ERROR });
|
||||
dispatch<ApiErrorAction>({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
||||
export const filterDomains = (searchTerm: string): FilterDomainsAction => ({ type: FILTER_DOMAINS, searchTerm });
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import Bottle from 'bottlejs';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { listDomains } from '../reducers/domainsList';
|
||||
import { filterDomains, listDomains } from '../reducers/domainsList';
|
||||
import { DomainSelector } from '../DomainSelector';
|
||||
import { ManageDomains } from '../ManageDomains';
|
||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
|
@ -10,10 +11,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
bottle.decorator('DomainSelector', connect([ 'domainsList' ], [ 'listDomains' ]));
|
||||
|
||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||
bottle.decorator('ManageDomains', connect([ 'domainsList' ], [ 'listDomains' ]));
|
||||
bottle.decorator('ManageDomains', connect(
|
||||
[ 'domainsList' ],
|
||||
[ 'listDomains', 'filterDomains', 'editDomainRedirects' ],
|
||||
));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('listDomains', listDomains, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('filterDomains', () => filterDomains);
|
||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'buildShlinkApiClient');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
|
|
|
@ -30,11 +30,7 @@ const SearchBar = (colorGenerator: ColorGenerator) => ({ listShortUrls, shortUrl
|
|||
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<SearchField
|
||||
onChange={
|
||||
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
|
||||
}
|
||||
/>
|
||||
<SearchField onChange={(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })} />
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="row">
|
||||
|
|
|
@ -5,6 +5,7 @@ import { buildReducer, buildActionCreator } from '../../utils/helpers/redux';
|
|||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { ProblemDetailsError } from '../../api/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
||||
|
@ -24,17 +25,13 @@ export interface CreateShortUrlAction extends Action<string> {
|
|||
result: ShortUrl;
|
||||
}
|
||||
|
||||
export interface CreateShortUrlFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlCreation = {
|
||||
result: null,
|
||||
saving: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<ShortUrlCreation, CreateShortUrlAction & CreateShortUrlFailedAction>({
|
||||
export default buildReducer<ShortUrlCreation, CreateShortUrlAction & ApiErrorAction>({
|
||||
[CREATE_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||
[CREATE_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||
[CREATE_SHORT_URL]: (_, { result }) => ({ result, saving: false, error: false }),
|
||||
|
@ -53,7 +50,7 @@ export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
|||
|
||||
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
|
||||
} catch (e) {
|
||||
dispatch<CreateShortUrlFailedAction>({ type: CREATE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: CREATE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ProblemDetailsError } from '../../api/types';
|
|||
import { GetState } from '../../container/types';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
||||
|
@ -24,17 +25,13 @@ export interface DeleteShortUrlAction extends Action<string> {
|
|||
domain?: string | null;
|
||||
}
|
||||
|
||||
interface DeleteShortUrlErrorAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlDeletion = {
|
||||
shortCode: '',
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<ShortUrlDeletion, DeleteShortUrlAction & DeleteShortUrlErrorAction>({
|
||||
export default buildReducer<ShortUrlDeletion, DeleteShortUrlAction & ApiErrorAction>({
|
||||
[DELETE_SHORT_URL_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[DELETE_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, errorData, loading: false, error: true }),
|
||||
[SHORT_URL_DELETED]: (state, { shortCode }) => ({ ...state, shortCode, loading: false, error: false }),
|
||||
|
@ -52,7 +49,7 @@ export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
|||
await deleteShortUrl(shortCode, domain);
|
||||
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
|
||||
} catch (e) {
|
||||
dispatch<DeleteShortUrlErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { GetState } from '../../container/types';
|
|||
import { shortUrlMatches } from '../helpers';
|
||||
import { ProblemDetailsError } from '../../api/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_SHORT_URL_DETAIL_START = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL_START';
|
||||
|
@ -25,16 +26,12 @@ export interface ShortUrlDetailAction extends Action<string> {
|
|||
shortUrl: ShortUrl;
|
||||
}
|
||||
|
||||
export interface ShortUrlDetailFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlDetail = {
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<ShortUrlDetail, ShortUrlDetailAction & ShortUrlDetailFailedAction>({
|
||||
export default buildReducer<ShortUrlDetail, ShortUrlDetailAction & ApiErrorAction>({
|
||||
[GET_SHORT_URL_DETAIL_START]: () => ({ loading: true, error: false }),
|
||||
[GET_SHORT_URL_DETAIL_ERROR]: (_, { errorData }) => ({ loading: false, error: true, errorData }),
|
||||
[GET_SHORT_URL_DETAIL]: (_, { shortUrl }) => ({ shortUrl, ...initialState }),
|
||||
|
@ -54,6 +51,6 @@ export const getShortUrlDetail = (buildShlinkApiClient: ShlinkApiClientBuilder)
|
|||
|
||||
dispatch<ShortUrlDetailAction>({ shortUrl, type: GET_SHORT_URL_DETAIL });
|
||||
} catch (e) {
|
||||
dispatch<ShortUrlDetailFailedAction>({ type: GET_SHORT_URL_DETAIL_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: GET_SHORT_URL_DETAIL_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
|||
import { ProblemDetailsError } from '../../api/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { supportsTagsInPatch } from '../../utils/helpers/features';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
|
||||
|
@ -25,16 +26,12 @@ export interface ShortUrlEditedAction extends Action<string> {
|
|||
shortUrl: ShortUrl;
|
||||
}
|
||||
|
||||
export interface ShortUrlEditionFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlEdition = {
|
||||
saving: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<ShortUrlEdition, ShortUrlEditedAction & ShortUrlEditionFailedAction>({
|
||||
export default buildReducer<ShortUrlEdition, ShortUrlEditedAction & ApiErrorAction>({
|
||||
[EDIT_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||
[EDIT_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||
[SHORT_URL_EDITED]: (_, { shortUrl }) => ({ shortUrl, saving: false, error: false }),
|
||||
|
@ -59,7 +56,7 @@ export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
|||
|
||||
dispatch<ShortUrlEditedAction>({ shortUrl, type: SHORT_URL_EDITED });
|
||||
} catch (e) {
|
||||
dispatch<ShortUrlEditionFailedAction>({ type: EDIT_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: EDIT_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
|||
forceListTags();
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
if (tagsList.loading) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (tagsList.error) {
|
||||
return (
|
||||
<Result type="error">
|
||||
|
@ -73,7 +73,7 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
|||
|
||||
return (
|
||||
<>
|
||||
{!tagsList.loading && <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />}
|
||||
<SearchField className="mb-3" onChange={filterTags} />
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { GetState } from '../../container/types';
|
|||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { ProblemDetailsError } from '../../api/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
||||
|
@ -22,16 +23,12 @@ export interface DeleteTagAction extends Action<string> {
|
|||
tag: string;
|
||||
}
|
||||
|
||||
export interface DeleteTagFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: TagDeletion = {
|
||||
deleting: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<TagDeletion, DeleteTagFailedAction>({
|
||||
export default buildReducer<TagDeletion, ApiErrorAction>({
|
||||
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
||||
[DELETE_TAG_ERROR]: (_, { errorData }) => ({ deleting: false, error: true, errorData }),
|
||||
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
||||
|
@ -48,7 +45,7 @@ export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag:
|
|||
await deleteTags([ tag ]);
|
||||
dispatch({ type: DELETE_TAG });
|
||||
} catch (e) {
|
||||
dispatch<DeleteTagFailedAction>({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import ColorGenerator from '../../utils/services/ColorGenerator';
|
|||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { ProblemDetailsError } from '../../api/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||
|
@ -29,10 +30,6 @@ export interface EditTagAction extends Action<string> {
|
|||
color: string;
|
||||
}
|
||||
|
||||
export interface EditTagFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: TagEdition = {
|
||||
oldName: '',
|
||||
newName: '',
|
||||
|
@ -40,7 +37,7 @@ const initialState: TagEdition = {
|
|||
error: false,
|
||||
};
|
||||
|
||||
export default buildReducer<TagEdition, EditTagAction & EditTagFailedAction>({
|
||||
export default buildReducer<TagEdition, EditTagAction & ApiErrorAction>({
|
||||
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
||||
[EDIT_TAG_ERROR]: (state, { errorData }) => ({ ...state, editing: false, error: true, errorData }),
|
||||
[EDIT_TAG]: (_, action) => ({
|
||||
|
@ -63,7 +60,7 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener
|
|||
colorGenerator.setColorForKey(newName, color);
|
||||
dispatch({ type: EDIT_TAG, oldName, newName });
|
||||
} catch (e) {
|
||||
dispatch<EditTagFailedAction>({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
|||
import { CreateVisit, Stats } from '../../visits/types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { TagStats } from '../data';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||
|
||||
|
@ -34,20 +35,16 @@ interface ListTagsAction extends Action<string> {
|
|||
stats: TagsStatsMap;
|
||||
}
|
||||
|
||||
interface ListTagsFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
interface FilterTagsAction extends Action<string> {
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
type ListTagsCombinedAction = ListTagsAction
|
||||
type TagsCombinedAction = ListTagsAction
|
||||
& DeleteTagAction
|
||||
& CreateVisitsAction
|
||||
& EditTagAction
|
||||
& FilterTagsAction
|
||||
& ListTagsFailedAction;
|
||||
& ApiErrorAction;
|
||||
|
||||
const initialState = {
|
||||
tags: [],
|
||||
|
@ -83,7 +80,7 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
|
|||
}, {}),
|
||||
);
|
||||
|
||||
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
||||
export default buildReducer<TagsList, TagsCombinedAction>({
|
||||
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
||||
[LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
||||
|
@ -130,7 +127,7 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
|
|||
|
||||
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
|
||||
} catch (e) {
|
||||
dispatch<ListTagsFailedAction>({ type: LIST_TAGS_ERROR, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: LIST_TAGS_ERROR, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8,12 +8,14 @@ interface FormGroupContainerProps {
|
|||
id?: string;
|
||||
type?: InputType;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FormGroupContainer: FC<FormGroupContainerProps> = (
|
||||
{ children, value, onChange, id = uuid(), type = 'text', required = true },
|
||||
{ children, value, onChange, id = uuid(), type = 'text', required = true, placeholder, className = '' },
|
||||
) => (
|
||||
<div className="form-group">
|
||||
<div className={`form-group ${className}`}>
|
||||
<label htmlFor={id} className="create-server__label">
|
||||
{children}:
|
||||
</label>
|
||||
|
@ -23,6 +25,7 @@ export const FormGroupContainer: FC<FormGroupContainerProps> = (
|
|||
id={id}
|
||||
value={value}
|
||||
required={required}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -10,14 +10,11 @@ let timer: NodeJS.Timeout | null;
|
|||
interface SearchFieldProps {
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
large?: boolean;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
const SearchField = (
|
||||
{ onChange, className, placeholder = 'Search...', large = true, noBorder = false }: SearchFieldProps,
|
||||
) => {
|
||||
const SearchField = ({ onChange, className, large = true, noBorder = false }: SearchFieldProps) => {
|
||||
const [ searchTerm, setSearchTerm ] = useState('');
|
||||
|
||||
const resetTimer = () => {
|
||||
|
@ -43,7 +40,7 @@ const SearchField = (
|
|||
'form-control-lg': large,
|
||||
'search-field__input--no-border': noBorder,
|
||||
})}
|
||||
placeholder={placeholder}
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => searchTermChanged(e.target.value)}
|
||||
/>
|
||||
|
|
|
@ -43,3 +43,5 @@ export type OptionalString = Optional<string>;
|
|||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
||||
|
||||
export const nonEmptyValueOrNull = <T>(value: T): T | null => isEmpty(value) ? null : value;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { ShlinkPaginator, ShlinkVisits } from '../../api/types';
|
||||
import { Visit, VisitsLoadFailedAction } from '../types';
|
||||
import { Visit } from '../types';
|
||||
import { parseApiError } from '../../api/utils';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
|
||||
const ITEMS_PER_PAGE = 5000;
|
||||
const PARALLEL_REQUESTS_COUNT = 4;
|
||||
|
@ -72,6 +73,6 @@ export const getVisitsWithLoader = async <T extends Action<string> & { visits: V
|
|||
|
||||
dispatch({ ...extraFinishActionData, visits, type: actionMap.finish });
|
||||
} catch (e) {
|
||||
dispatch<VisitsLoadFailedAction>({ type: actionMap.error, errorData: parseApiError(e) });
|
||||
dispatch<ApiErrorAction>({ type: actionMap.error, errorData: parseApiError(e) });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import {
|
||||
OrphanVisit,
|
||||
OrphanVisitType,
|
||||
Visit,
|
||||
VisitsInfo,
|
||||
VisitsLoadFailedAction,
|
||||
VisitsLoadProgressChangedAction,
|
||||
} from '../types';
|
||||
import { OrphanVisit, OrphanVisitType, Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ShlinkVisitsParams } from '../../api/types';
|
||||
import { isOrphanVisit } from '../types/helpers';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { getVisitsWithLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
|
@ -31,7 +25,7 @@ export interface OrphanVisitsAction extends Action<string> {
|
|||
type OrphanVisitsCombinedAction = OrphanVisitsAction
|
||||
& VisitsLoadProgressChangedAction
|
||||
& CreateVisitsAction
|
||||
& VisitsLoadFailedAction;
|
||||
& ApiErrorAction;
|
||||
|
||||
const initialState: VisitsInfo = {
|
||||
visits: [],
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { shortUrlMatches } from '../../short-urls/helpers';
|
||||
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { ShortUrlIdentifier } from '../../short-urls/data';
|
||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ShlinkVisitsParams } from '../../api/types';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { getVisitsWithLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
|
@ -27,7 +28,7 @@ interface ShortUrlVisitsAction extends Action<string>, ShortUrlIdentifier {
|
|||
type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction
|
||||
& VisitsLoadProgressChangedAction
|
||||
& CreateVisitsAction
|
||||
& VisitsLoadFailedAction;
|
||||
& ApiErrorAction;
|
||||
|
||||
const initialState: ShortUrlVisits = {
|
||||
visits: [],
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ShlinkVisitsParams } from '../../api/types';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { getVisitsWithLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
|
@ -28,7 +29,7 @@ export interface TagVisitsAction extends Action<string> {
|
|||
type TagsVisitsCombinedAction = TagVisitsAction
|
||||
& VisitsLoadProgressChangedAction
|
||||
& CreateVisitsAction
|
||||
& VisitsLoadFailedAction;
|
||||
& ApiErrorAction;
|
||||
|
||||
const initialState: TagVisits = {
|
||||
visits: [],
|
||||
|
|
|
@ -17,10 +17,6 @@ export interface VisitsLoadProgressChangedAction extends Action<string> {
|
|||
progress: number;
|
||||
}
|
||||
|
||||
export interface VisitsLoadFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export type OrphanVisitType = 'base_url' | 'invalid_short_url' | 'regular_404';
|
||||
|
||||
interface VisitLocation {
|
||||
|
|
|
@ -13,20 +13,26 @@ describe('domainsList', () => {
|
|||
const domains = [ Mock.all<ShlinkDomain>(), Mock.all<ShlinkDomain>(), Mock.all<ShlinkDomain>() ];
|
||||
|
||||
describe('reducer', () => {
|
||||
const action = (type: string, args: Partial<ListDomainsAction> = {}) => Mock.of<ListDomainsAction>(
|
||||
const action = (type: string, args: Partial<ListDomainsAction> = {}): any => Mock.of<ListDomainsAction>(
|
||||
{ type, ...args },
|
||||
);
|
||||
|
||||
it('returns loading on LIST_DOMAINS_START', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_START))).toEqual({ domains: [], loading: true, error: false });
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_START))).toEqual(
|
||||
{ domains: [], filteredDomains: [], loading: true, error: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error on LIST_DOMAINS_ERROR', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_ERROR))).toEqual({ domains: [], loading: false, error: true });
|
||||
expect(reducer(undefined, action(LIST_DOMAINS_ERROR))).toEqual(
|
||||
{ domains: [], filteredDomains: [], loading: false, error: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns domains on LIST_DOMAINS', () => {
|
||||
expect(reducer(undefined, action(LIST_DOMAINS, { domains }))).toEqual({ domains, loading: false, error: false });
|
||||
expect(reducer(undefined, action(LIST_DOMAINS, { domains }))).toEqual(
|
||||
{ domains, filteredDomains: domains, loading: false, error: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { determineOrderDir, rangeOf } from '../../src/utils/utils';
|
||||
import { determineOrderDir, nonEmptyValueOrNull, rangeOf } from '../../src/utils/utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('determineOrderDir', () => {
|
||||
|
@ -47,4 +47,17 @@ describe('utils', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonEmptyValueOrNull', () => {
|
||||
it.each([
|
||||
[ '', null ],
|
||||
[ 'Hello', 'Hello' ],
|
||||
[[], null ],
|
||||
[[ 1, 2, 3 ], [ 1, 2, 3 ]],
|
||||
[{}, null ],
|
||||
[{ foo: 'bar' }, { foo: 'bar' }],
|
||||
])('returns expected value based on input', (value, expected) => {
|
||||
expect(nonEmptyValueOrNull(value)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue