mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-14 19:43:49 +00:00
Extract shlink-web-component outside of src folder
This commit is contained in:
63
shlink-web-component/domains/DomainRow.tsx
Normal file
63
shlink-web-component/domains/DomainRow.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import type { OptionalString } from '../../src/utils/utils';
|
||||
import type { ShlinkDomainRedirects } from '../api-contract';
|
||||
import type { Domain } from './data';
|
||||
import { DomainDropdown } from './helpers/DomainDropdown';
|
||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||
import type { EditDomainRedirects } from './reducers/domainRedirects';
|
||||
|
||||
interface DomainRowProps {
|
||||
domain: Domain;
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => 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 fixedWidth icon={defaultDomainIcon} className="text-primary" id="defaultDomainIcon" />
|
||||
<UncontrolledTooltip target="defaultDomainIcon" placement="right">Default domain</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
export const DomainRow: FC<DomainRowProps> = (
|
||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects },
|
||||
) => {
|
||||
const { domain: authority, isDefault, redirects, status } = domain;
|
||||
|
||||
useEffect(() => {
|
||||
checkDomainHealth(domain.domain);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<tr className="responsive-table__row">
|
||||
<td className="responsive-table__cell" data-th="Is default domain">{isDefault && <DefaultDomain />}</td>
|
||||
<th className="responsive-table__cell" data-th="Domain">{authority}</th>
|
||||
<td className="responsive-table__cell" data-th="Base path redirect">
|
||||
{redirects?.baseUrlRedirect ?? <Nr fallback={defaultRedirects?.baseUrlRedirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell" data-th="Regular 404 redirect">
|
||||
{redirects?.regular404Redirect ?? <Nr fallback={defaultRedirects?.regular404Redirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell" data-th="Invalid short URL redirect">
|
||||
{redirects?.invalidShortUrlRedirect ?? <Nr fallback={defaultRedirects?.invalidShortUrlRedirect} />}
|
||||
</td>
|
||||
<td className="responsive-table__cell text-lg-center" data-th="Status">
|
||||
<DomainStatusIcon status={status} />
|
||||
</td>
|
||||
<td className="responsive-table__cell text-end">
|
||||
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
19
shlink-web-component/domains/DomainSelector.scss
Normal file
19
shlink-web-component/domains/DomainSelector.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@import '../../src/utils/base';
|
||||
@import '../../src/utils/mixins/vertical-align';
|
||||
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn,
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:hover,
|
||||
.domains-dropdown__toggle-btn.domains-dropdown__toggle-btn:active {
|
||||
color: $textPlaceholder !important;
|
||||
}
|
||||
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active,
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:hover,
|
||||
.domains-dropdown__toggle-btn--active.domains-dropdown__toggle-btn--active:active {
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
|
||||
.domains-dropdown__back-btn.domains-dropdown__back-btn,
|
||||
.domains-dropdown__back-btn.domains-dropdown__back-btn:hover {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
74
shlink-web-component/domains/DomainSelector.tsx
Normal file
74
shlink-web-component/domains/DomainSelector.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { faUndo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { isEmpty, pipe } from 'ramda';
|
||||
import { useEffect } from 'react';
|
||||
import type { InputProps } from 'reactstrap';
|
||||
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
|
||||
import { DropdownBtn } from '../../src/utils/DropdownBtn';
|
||||
import { useToggle } from '../../src/utils/helpers/hooks';
|
||||
import type { DomainsList } from './reducers/domainsList';
|
||||
import './DomainSelector.scss';
|
||||
|
||||
export interface DomainSelectorProps extends Omit<InputProps, 'onChange'> {
|
||||
value?: string;
|
||||
onChange: (domain: string) => void;
|
||||
}
|
||||
|
||||
interface DomainSelectorConnectProps extends DomainSelectorProps {
|
||||
listDomains: Function;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
export const DomainSelector = ({ listDomains, value, domainsList, onChange }: DomainSelectorConnectProps) => {
|
||||
const [inputDisplayed,, showInput, hideInput] = useToggle();
|
||||
const { domains } = domainsList;
|
||||
const valueIsEmpty = isEmpty(value);
|
||||
const unselectDomain = () => onChange('');
|
||||
|
||||
useEffect(() => {
|
||||
listDomains();
|
||||
}, []);
|
||||
|
||||
return inputDisplayed ? (
|
||||
<InputGroup>
|
||||
<Input
|
||||
value={value ?? ''}
|
||||
placeholder="Domain"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
id="backToDropdown"
|
||||
outline
|
||||
type="button"
|
||||
className="domains-dropdown__back-btn"
|
||||
aria-label="Back to domains list"
|
||||
onClick={pipe(unselectDomain, hideInput)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</Button>
|
||||
<UncontrolledTooltip target="backToDropdown" placement="left" trigger="hover">
|
||||
Existing domains
|
||||
</UncontrolledTooltip>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<DropdownBtn
|
||||
text={valueIsEmpty ? 'Domain' : `Domain: ${value}`}
|
||||
className={!valueIsEmpty ? 'domains-dropdown__toggle-btn--active' : 'domains-dropdown__toggle-btn'}
|
||||
>
|
||||
{domains.map(({ domain, isDefault }) => (
|
||||
<DropdownItem
|
||||
key={domain}
|
||||
active={(value === domain || isDefault) && valueIsEmpty}
|
||||
onClick={() => onChange(domain)}
|
||||
>
|
||||
{domain}
|
||||
{isDefault && <span className="float-end text-muted">default</span>}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem onClick={pipe(unselectDomain, showInput)}>
|
||||
<i>New domain</i>
|
||||
</DropdownItem>
|
||||
</DropdownBtn>
|
||||
);
|
||||
};
|
||||
74
shlink-web-component/domains/ManageDomains.tsx
Normal file
74
shlink-web-component/domains/ManageDomains.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { ShlinkApiError } from '../../src/api/ShlinkApiError';
|
||||
import { Message } from '../../src/utils/Message';
|
||||
import { Result } from '../../src/utils/Result';
|
||||
import { SearchField } from '../../src/utils/SearchField';
|
||||
import { SimpleCard } from '../../src/utils/SimpleCard';
|
||||
import { DomainRow } from './DomainRow';
|
||||
import type { EditDomainRedirects } from './reducers/domainRedirects';
|
||||
import type { DomainsList } from './reducers/domainsList';
|
||||
|
||||
interface ManageDomainsProps {
|
||||
listDomains: Function;
|
||||
filterDomains: (searchTerm: string) => void;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => void;
|
||||
domainsList: DomainsList;
|
||||
}
|
||||
|
||||
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
|
||||
|
||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth },
|
||||
) => {
|
||||
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
||||
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||
|
||||
useEffect(() => {
|
||||
listDomains();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Result type="error">
|
||||
<ShlinkApiError errorData={errorData} fallbackMessage="Error loading domains :(" />
|
||||
</Result>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleCard>
|
||||
<table className="table table-hover responsive-table mb-0">
|
||||
<thead className="responsive-table__header">
|
||||
<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) => (
|
||||
<DomainRow
|
||||
key={domain.domain}
|
||||
domain={domain}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
checkDomainHealth={checkDomainHealth}
|
||||
defaultRedirects={resolvedDefaultRedirects}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</SimpleCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchField className="mb-3" onChange={filterDomains} />
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
7
shlink-web-component/domains/data/index.ts
Normal file
7
shlink-web-component/domains/data/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ShlinkDomain } from '../../api-contract';
|
||||
|
||||
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
||||
|
||||
export interface Domain extends ShlinkDomain {
|
||||
status: DomainStatus;
|
||||
}
|
||||
49
shlink-web-component/domains/helpers/DomainDropdown.tsx
Normal file
49
shlink-web-component/domains/helpers/DomainDropdown.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { useToggle } from '../../../src/utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
|
||||
import { useFeature } from '../../utils/features';
|
||||
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||
import type { Domain } from '../data';
|
||||
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
||||
|
||||
interface DomainDropdownProps {
|
||||
domain: Domain;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects }) => {
|
||||
const [isModalOpen, toggleModal] = useToggle();
|
||||
const { isDefault } = domain;
|
||||
const canBeEdited = !isDefault;
|
||||
const withVisits = useFeature('domainVisits');
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
|
||||
return (
|
||||
<RowDropdownBtn>
|
||||
{withVisits && (
|
||||
<DropdownItem
|
||||
tag={Link}
|
||||
to={`${routesPrefix}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
||||
>
|
||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
|
||||
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
||||
</DropdownItem>
|
||||
|
||||
<EditDomainRedirectsModal
|
||||
domain={domain}
|
||||
isOpen={isModalOpen}
|
||||
toggle={toggleModal}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</RowDropdownBtn>
|
||||
);
|
||||
};
|
||||
60
shlink-web-component/domains/helpers/DomainStatusIcon.tsx
Normal file
60
shlink-web-component/domains/helpers/DomainStatusIcon.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
faCheck as checkIcon,
|
||||
faCircleNotch as loadingStatusIcon,
|
||||
faTimes as invalidIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { useElementRef } from '../../../src/utils/helpers/hooks';
|
||||
import type { MediaMatcher } from '../../../src/utils/types';
|
||||
import type { DomainStatus } from '../data';
|
||||
|
||||
interface DomainStatusIconProps {
|
||||
status: DomainStatus;
|
||||
matchMedia?: MediaMatcher;
|
||||
}
|
||||
|
||||
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
|
||||
const ref = useElementRef<HTMLSpanElement>();
|
||||
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
|
||||
const [isMobile, setIsMobile] = useState<boolean>(matchesMobile());
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => setIsMobile(matchesMobile());
|
||||
|
||||
window.addEventListener('resize', listener);
|
||||
|
||||
return () => window.removeEventListener('resize', listener);
|
||||
}, []);
|
||||
|
||||
if (status === 'validating') {
|
||||
return <FontAwesomeIcon fixedWidth icon={loadingStatusIcon} spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span ref={ref}>
|
||||
{status === 'valid'
|
||||
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
|
||||
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
|
||||
</span>
|
||||
<UncontrolledTooltip
|
||||
target={ref}
|
||||
placement={isMobile ? 'top-start' : 'left'}
|
||||
autohide={status === 'valid'}
|
||||
>
|
||||
{status === 'valid' ? 'Congratulations! This domain is properly configured.' : (
|
||||
<span>
|
||||
Oops! There is some missing configuration, and short URLs shared with this domain will not work.
|
||||
<br />
|
||||
Check the <ExternalLink href="https://slnk.to/multi-domain-docs">documentation</ExternalLink> in order to
|
||||
find out what is missing.
|
||||
</span>
|
||||
)}
|
||||
</UncontrolledTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { FC } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import type { ShlinkDomain } from '../../../api/types';
|
||||
import type { InputFormGroupProps } from '../../../src/utils/forms/InputFormGroup';
|
||||
import { InputFormGroup } from '../../../src/utils/forms/InputFormGroup';
|
||||
import { InfoTooltip } from '../../../src/utils/InfoTooltip';
|
||||
import { handleEventPreventingDefault, nonEmptyValueOrNull } from '../../../src/utils/utils';
|
||||
import type { EditDomainRedirects } from '../reducers/domainRedirects';
|
||||
|
||||
interface EditDomainRedirectsModalProps {
|
||||
domain: ShlinkDomain;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
}
|
||||
|
||||
const FormGroup: FC<InputFormGroupProps & { isLast?: boolean }> = ({ isLast, ...rest }) => (
|
||||
<InputFormGroup
|
||||
{...rest}
|
||||
required={false}
|
||||
type="url"
|
||||
placeholder="No redirect"
|
||||
className={isLast ? 'mb-0' : ''}
|
||||
/>
|
||||
);
|
||||
|
||||
export const EditDomainRedirectsModal: FC<EditDomainRedirectsModalProps> = (
|
||||
{ isOpen, toggle, domain, editDomainRedirects },
|
||||
) => {
|
||||
const [baseUrlRedirect, setBaseUrlRedirect] = useState(domain.redirects?.baseUrlRedirect ?? '');
|
||||
const [regular404Redirect, setRegular404Redirect] = useState(domain.redirects?.regular404Redirect ?? '');
|
||||
const [invalidShortUrlRedirect, setInvalidShortUrlRedirect] = useState(
|
||||
domain.redirects?.invalidShortUrlRedirect ?? '',
|
||||
);
|
||||
const handleSubmit = handleEventPreventingDefault(async () => editDomainRedirects({
|
||||
domain: domain.domain,
|
||||
redirects: {
|
||||
baseUrlRedirect: nonEmptyValueOrNull(baseUrlRedirect),
|
||||
regular404Redirect: nonEmptyValueOrNull(regular404Redirect),
|
||||
invalidShortUrlRedirect: nonEmptyValueOrNull(invalidShortUrlRedirect),
|
||||
},
|
||||
}).then(toggle));
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<form name="domainRedirectsModal" onSubmit={handleSubmit}>
|
||||
<ModalHeader toggle={toggle}>Edit redirects for <b>{domain.domain}</b></ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup value={baseUrlRedirect} onChange={setBaseUrlRedirect}>
|
||||
<InfoTooltip className="me-2" placement="bottom">
|
||||
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 className="me-2" placement="bottom">
|
||||
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 className="me-2" placement="bottom">
|
||||
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>
|
||||
);
|
||||
};
|
||||
19
shlink-web-component/domains/reducers/domainRedirects.ts
Normal file
19
shlink-web-component/domains/reducers/domainRedirects.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
||||
|
||||
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||
|
||||
export interface EditDomainRedirects {
|
||||
domain: string;
|
||||
redirects: ShlinkDomainRedirects;
|
||||
}
|
||||
|
||||
export const editDomainRedirects = (
|
||||
apiClient: ShlinkApiClient,
|
||||
) => createAsyncThunk(
|
||||
EDIT_DOMAIN_REDIRECTS,
|
||||
async ({ domain, redirects: providedRedirects }: EditDomainRedirects): Promise<EditDomainRedirects> => {
|
||||
const redirects = await apiClient.editDomainRedirects({ domain, ...providedRedirects });
|
||||
return { domain, redirects };
|
||||
},
|
||||
);
|
||||
108
shlink-web-component/domains/reducers/domainsList.ts
Normal file
108
shlink-web-component/domains/reducers/domainsList.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { Domain, DomainStatus } from '../data';
|
||||
import type { EditDomainRedirects } from './domainRedirects';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/domainsList';
|
||||
|
||||
export interface DomainsList {
|
||||
domains: Domain[];
|
||||
filteredDomains: Domain[];
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
interface ListDomains {
|
||||
domains: Domain[];
|
||||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
}
|
||||
|
||||
interface ValidateDomain {
|
||||
domain: string;
|
||||
status: DomainStatus;
|
||||
}
|
||||
|
||||
const initialState: DomainsList = {
|
||||
domains: [],
|
||||
filteredDomains: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
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 const domainsListReducerCreator = (
|
||||
apiClient: ShlinkApiClient,
|
||||
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
||||
) => {
|
||||
const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (): Promise<ListDomains> => {
|
||||
const { data, defaultRedirects } = await apiClient.listDomains();
|
||||
|
||||
return {
|
||||
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||
defaultRedirects,
|
||||
};
|
||||
});
|
||||
|
||||
const checkDomainHealth = createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/checkDomainHealth`,
|
||||
async (domain: string): Promise<ValidateDomain> => {
|
||||
try {
|
||||
const { status } = await apiClient.health(domain);
|
||||
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
|
||||
} catch (e) {
|
||||
return { domain, status: 'invalid' };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const filterDomains = createAction<string>(`${REDUCER_PREFIX}/filterDomains`);
|
||||
|
||||
const { reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
|
||||
name: REDUCER_PREFIX,
|
||||
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,
|
||||
};
|
||||
};
|
||||
34
shlink-web-component/domains/services/provideServices.ts
Normal file
34
shlink-web-component/domains/services/provideServices.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type Bottle from 'bottlejs';
|
||||
import { prop } from 'ramda';
|
||||
import type { ConnectDecorator } from '../../container';
|
||||
import { DomainSelector } from '../DomainSelector';
|
||||
import { ManageDomains } from '../ManageDomains';
|
||||
import { editDomainRedirects } from '../reducers/domainRedirects';
|
||||
import { domainsListReducerCreator } from '../reducers/domainsList';
|
||||
|
||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
bottle.serviceFactory('DomainSelector', () => DomainSelector);
|
||||
bottle.decorator('DomainSelector', connect(['domainsList'], ['listDomains']));
|
||||
|
||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||
bottle.decorator('ManageDomains', connect(
|
||||
['domainsList'],
|
||||
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
||||
));
|
||||
|
||||
// Reducer
|
||||
bottle.serviceFactory(
|
||||
'domainsListReducerCreator',
|
||||
domainsListReducerCreator,
|
||||
'apiClient',
|
||||
'editDomainRedirects',
|
||||
);
|
||||
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
|
||||
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
|
||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'apiClient');
|
||||
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
||||
};
|
||||
Reference in New Issue
Block a user