mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-22 17:10:26 +03:00
Decouple shlink-web-component from the concept of servers
This commit is contained in:
parent
5f6dc186e3
commit
21525ef945
22 changed files with 55 additions and 82 deletions
|
@ -3,9 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import type { ShlinkDomainRedirects } from '../../api/types';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
import type { ShlinkDomainRedirects } from '../api-contract';
|
||||
import type { Domain } from './data';
|
||||
import { DomainDropdown } from './helpers/DomainDropdown';
|
||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||
|
@ -16,7 +15,6 @@ interface DomainRowProps {
|
|||
defaultRedirects?: ShlinkDomainRedirects;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => void;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => (
|
||||
|
@ -33,7 +31,7 @@ const DefaultDomain: FC = () => (
|
|||
);
|
||||
|
||||
export const DomainRow: FC<DomainRowProps> = (
|
||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects },
|
||||
) => {
|
||||
const { domain: authority, isDefault, redirects, status } = domain;
|
||||
|
||||
|
@ -58,7 +56,7 @@ export const DomainRow: FC<DomainRowProps> = (
|
|||
<DomainStatusIcon status={status} />
|
||||
</td>
|
||||
<td className="responsive-table__cell text-end">
|
||||
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} selectedServer={selectedServer} />
|
||||
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import { Message } from '../../utils/Message';
|
||||
import { Result } from '../../utils/Result';
|
||||
import { SearchField } from '../../utils/SearchField';
|
||||
|
@ -16,13 +15,12 @@ interface ManageDomainsProps {
|
|||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
checkDomainHealth: (domain: string) => void;
|
||||
domainsList: DomainsList;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const headers = ['', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '', ''];
|
||||
|
||||
export const ManageDomains: FC<ManageDomainsProps> = (
|
||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth, selectedServer },
|
||||
{ listDomains, domainsList, filterDomains, editDomainRedirects, checkDomainHealth },
|
||||
) => {
|
||||
const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList;
|
||||
const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects;
|
||||
|
@ -59,7 +57,6 @@ export const ManageDomains: FC<ManageDomainsProps> = (
|
|||
editDomainRedirects={editDomainRedirects}
|
||||
checkDomainHealth={checkDomainHealth}
|
||||
defaultRedirects={resolvedDefaultRedirects}
|
||||
selectedServer={selectedServer}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ShlinkDomain } from '../../../api/types';
|
||||
import type { ShlinkDomain } from '../../api-contract';
|
||||
|
||||
export type DomainStatus = 'validating' | 'valid' | 'invalid';
|
||||
|
||||
|
|
|
@ -3,11 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import type { SelectedServer } from '../../../servers/data';
|
||||
import { getServerId } from '../../../servers/data';
|
||||
import { useToggle } from '../../../utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../../../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';
|
||||
|
@ -16,22 +15,21 @@ import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
|||
interface DomainDropdownProps {
|
||||
domain: Domain;
|
||||
editDomainRedirects: (redirects: EditDomainRedirects) => Promise<void>;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
|
||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects }) => {
|
||||
const [isModalOpen, toggleModal] = useToggle();
|
||||
const { isDefault } = domain;
|
||||
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition');
|
||||
const withVisits = useFeature('domainVisits');
|
||||
const serverId = getServerId(selectedServer);
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
|
||||
return (
|
||||
<RowDropdownBtn>
|
||||
{withVisits && (
|
||||
<DropdownItem
|
||||
tag={Link}
|
||||
to={`/server/${serverId}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
||||
to={`${routesPrefix}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
||||
>
|
||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||
</DropdownItem>
|
||||
|
|
|
@ -13,7 +13,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
bottle.serviceFactory('ManageDomains', () => ManageDomains);
|
||||
bottle.decorator('ManageDomains', connect(
|
||||
['domainsList', 'selectedServer'],
|
||||
['domainsList'],
|
||||
['listDomains', 'filterDomains', 'editDomainRedirects', 'checkDomainHealth'],
|
||||
));
|
||||
|
||||
|
|
|
@ -2,12 +2,8 @@ import type { FC } from 'react';
|
|||
import { useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
||||
import type { ShlinkShortUrlsListParams } from '../../api/types';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import { getServerId } from '../../servers/data';
|
||||
import { HighlightCard } from '../../servers/helpers/HighlightCard';
|
||||
import { VisitsHighlightCard } from '../../servers/helpers/VisitsHighlightCard';
|
||||
import { prettify } from '../../utils/helpers/numbers';
|
||||
import type { ShlinkShortUrlsListParams } from '../api-contract';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
||||
|
@ -16,15 +12,17 @@ import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
|
|||
import type { ShortUrlsTableType } from '../short-urls/ShortUrlsTable';
|
||||
import type { TagsList } from '../tags/reducers/tagsList';
|
||||
import { useFeature } from '../utils/features';
|
||||
import { useRoutesPrefix } from '../utils/routesPrefix';
|
||||
import { useSetting } from '../utils/settings';
|
||||
import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||
import { HighlightCard } from './helpers/HighlightCard';
|
||||
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
|
||||
|
||||
interface OverviewConnectProps {
|
||||
shortUrlsList: ShortUrlsListState;
|
||||
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
|
||||
listTags: Function;
|
||||
tagsList: TagsList;
|
||||
selectedServer: SelectedServer;
|
||||
visitsOverview: VisitsOverview;
|
||||
loadVisitsOverview: Function;
|
||||
}
|
||||
|
@ -37,14 +35,13 @@ export const Overview = (
|
|||
listShortUrls,
|
||||
listTags,
|
||||
tagsList,
|
||||
selectedServer,
|
||||
loadVisitsOverview,
|
||||
visitsOverview,
|
||||
}: OverviewConnectProps) => {
|
||||
const { loading, shortUrls } = shortUrlsList;
|
||||
const { loading: loadingTags } = tagsList;
|
||||
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
||||
const serverId = getServerId(selectedServer);
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
const linkToNonOrphanVisits = useFeature('nonOrphanVisits');
|
||||
const navigate = useNavigate();
|
||||
const visits = useSetting('visits');
|
||||
|
@ -61,7 +58,7 @@ export const Overview = (
|
|||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<VisitsHighlightCard
|
||||
title="Visits"
|
||||
link={linkToNonOrphanVisits ? `/server/${serverId}/non-orphan-visits` : undefined}
|
||||
link={linkToNonOrphanVisits ? `${routesPrefix}/non-orphan-visits` : undefined}
|
||||
excludeBots={visits?.excludeBots ?? false}
|
||||
loading={loadingVisits}
|
||||
visitsSummary={nonOrphanVisits}
|
||||
|
@ -70,19 +67,19 @@ export const Overview = (
|
|||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<VisitsHighlightCard
|
||||
title="Orphan visits"
|
||||
link={`/server/${serverId}/orphan-visits`}
|
||||
link={`${routesPrefix}/orphan-visits`}
|
||||
excludeBots={visits?.excludeBots ?? false}
|
||||
loading={loadingVisits}
|
||||
visitsSummary={orphanVisits}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
|
||||
<HighlightCard title="Short URLs" link={`${routesPrefix}/list-short-urls/1`}>
|
||||
{loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)}
|
||||
</HighlightCard>
|
||||
</div>
|
||||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<HighlightCard title="Tags" link={`/server/${serverId}/manage-tags`}>
|
||||
<HighlightCard title="Tags" link={`${routesPrefix}/manage-tags`}>
|
||||
{loadingTags ? 'Loading...' : prettify(tagsList.tags.length)}
|
||||
</HighlightCard>
|
||||
</div>
|
||||
|
@ -92,7 +89,7 @@ export const Overview = (
|
|||
<CardHeader>
|
||||
<span className="d-sm-none">Create a short URL</span>
|
||||
<h5 className="d-none d-sm-inline">Create a short URL</h5>
|
||||
<Link className="float-end" to={`/server/${serverId}/create-short-url`}>Advanced options »</Link>
|
||||
<Link className="float-end" to={`${routesPrefix}/create-short-url`}>Advanced options »</Link>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<CreateShortUrl basicMode />
|
||||
|
@ -102,13 +99,13 @@ export const Overview = (
|
|||
<CardHeader>
|
||||
<span className="d-sm-none">Recently created URLs</span>
|
||||
<h5 className="d-none d-sm-inline">Recently created URLs</h5>
|
||||
<Link className="float-end" to={`/server/${serverId}/list-short-urls/1`}>See all »</Link>
|
||||
<Link className="float-end" to={`${routesPrefix}/list-short-urls/1`}>See all »</Link>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ShortUrlsTable
|
||||
shortUrlsList={shortUrlsList}
|
||||
className="mb-0"
|
||||
onTagClick={(tag) => navigate(`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
|
||||
onTagClick={(tag) => navigate(`${routesPrefix}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../utils/base';
|
||||
@import '../../../utils/base';
|
||||
|
||||
.highlight-card.highlight-card {
|
||||
text-align: center;
|
|
@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
|
||||
import { useElementRef } from '../../utils/helpers/hooks';
|
||||
import { useElementRef } from '../../../utils/helpers/hooks';
|
||||
import './HighlightCard.scss';
|
||||
|
||||
export type HighlightCardProps = PropsWithChildren<{
|
|
@ -1,6 +1,6 @@
|
|||
import type { FC } from 'react';
|
||||
import type { PartialVisitsSummary } from '../../shlink-web-component/visits/reducers/visitsOverview';
|
||||
import { prettify } from '../../utils/helpers/numbers';
|
||||
import { prettify } from '../../../utils/helpers/numbers';
|
||||
import type { PartialVisitsSummary } from '../../visits/reducers/visitsOverview';
|
||||
import type { HighlightCardProps } from './HighlightCard';
|
||||
import { HighlightCard } from './HighlightCard';
|
||||
|
|
@ -5,7 +5,7 @@ import { Overview } from '../Overview';
|
|||
export function provideServices(bottle: Bottle, connect: ConnectDecorator) {
|
||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
||||
bottle.decorator('Overview', connect(
|
||||
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
|
||||
['shortUrlsList', 'tagsList', 'mercureInfo', 'visitsOverview'],
|
||||
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ReportExporter } from '../../../common/services/ReportExporter';
|
||||
import type { SelectedServer } from '../../../servers/data';
|
||||
import { isServerWithId } from '../../../servers/data';
|
||||
import { ExportBtn } from '../../../utils/ExportBtn';
|
||||
import { useToggle } from '../../../utils/helpers/hooks';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
|
@ -13,23 +11,15 @@ export interface ExportShortUrlsBtnProps {
|
|||
amount?: number;
|
||||
}
|
||||
|
||||
interface ExportShortUrlsBtnConnectProps extends ExportShortUrlsBtnProps {
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const itemsPerPage = 20;
|
||||
|
||||
export const ExportShortUrlsBtn = (
|
||||
apiClient: ShlinkApiClient,
|
||||
{ exportShortUrls }: ReportExporter,
|
||||
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
|
||||
): FC<ExportShortUrlsBtnProps> => ({ amount = 0 }) => {
|
||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
||||
const [loading,, startLoading, stopLoading] = useToggle();
|
||||
const exportAllUrls = useCallback(async () => {
|
||||
if (!isServerWithId(selectedServer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = amount / itemsPerPage;
|
||||
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
||||
const { data } = await apiClient.listShortUrls(
|
||||
|
@ -63,7 +53,7 @@ export const ExportShortUrlsBtn = (
|
|||
};
|
||||
}));
|
||||
stopLoading();
|
||||
}, [selectedServer]);
|
||||
}, []);
|
||||
|
||||
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { isEmpty, pipe } from 'ramda';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import type { TagsFilteringMode } from '../../../api/types';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { orderToString, stringToOrder } from '../../../utils/helpers/ordering';
|
||||
import { parseQuery, stringifyQuery } from '../../../utils/helpers/query';
|
||||
import type { BooleanString } from '../../../utils/utils';
|
||||
import { parseOptionalBooleanToString } from '../../../utils/utils';
|
||||
import type { TagsFilteringMode } from '../../api-contract';
|
||||
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';
|
||||
|
||||
interface ShortUrlsQueryCommon {
|
||||
|
@ -36,7 +37,7 @@ type ToFirstPage = (extra: Partial<ShortUrlsFiltering>) => void;
|
|||
export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const { serverId = '' } = useParams<{ serverId: string }>();
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
|
||||
const filtering = useMemo(
|
||||
pipe(
|
||||
|
@ -70,7 +71,7 @@ export const useShortUrlsQuery = (): [ShortUrlsFiltering, ToFirstPage] => {
|
|||
const stringifiedQuery = stringifyQuery(query);
|
||||
const queryString = isEmpty(stringifiedQuery) ? '' : `?${stringifiedQuery}`;
|
||||
|
||||
navigate(`/server/${serverId}/list-short-urls/1${queryString}`);
|
||||
navigate(`${routesPrefix}/list-short-urls/1${queryString}`);
|
||||
};
|
||||
|
||||
return [filtering, toFirstPageWithExtra];
|
||||
|
|
|
@ -55,7 +55,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||
|
||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClient', 'ReportExporter');
|
||||
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
||||
|
||||
// Reducers
|
||||
bottle.serviceFactory(
|
||||
|
|
|
@ -3,7 +3,6 @@ import type { FC } from 'react';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Row } from 'reactstrap';
|
||||
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import { determineOrderDir, sortList } from '../../utils/helpers/ordering';
|
||||
import { Message } from '../../utils/Message';
|
||||
import { OrderingDropdown } from '../../utils/OrderingDropdown';
|
||||
|
@ -22,11 +21,10 @@ export interface TagsListProps {
|
|||
filterTags: (searchTerm: string) => void;
|
||||
forceListTags: Function;
|
||||
tagsList: TagsListState;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
export const TagsList = (TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||
{ filterTags, forceListTags, tagsList, selectedServer }: TagsListProps,
|
||||
{ filterTags, forceListTags, tagsList }: TagsListProps,
|
||||
) => {
|
||||
const settings = useSettings();
|
||||
const [order, setOrder] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
|
||||
|
@ -78,7 +76,6 @@ export const TagsList = (TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
|||
return (
|
||||
<TagsTable
|
||||
sortedTags={sortedTags}
|
||||
selectedServer={selectedServer}
|
||||
currentOrder={order}
|
||||
orderByColumn={orderByColumn}
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface TagsTableProps extends TagsListChildrenProps {
|
|||
const TAGS_PER_PAGE = 20; // TODO Allow customizing this value in settings
|
||||
|
||||
export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
||||
{ sortedTags, selectedServer, orderByColumn, currentOrder }: TagsTableProps,
|
||||
{ sortedTags, orderByColumn, currentOrder }: TagsTableProps,
|
||||
) => {
|
||||
const isFirstLoad = useRef(true);
|
||||
const { search } = useLocation();
|
||||
|
@ -57,7 +57,7 @@ export const TagsTable = (TagsTableRow: FC<TagsTableRowProps>) => (
|
|||
</thead>
|
||||
<tbody>
|
||||
{currentPage.length === 0 && <tr><td colSpan={4} className="text-center">No results found</td></tr>}
|
||||
{currentPage.map((tag) => <TagsTableRow key={tag.tag} tag={tag} selectedServer={selectedServer} />)}
|
||||
{currentPage.map((tag) => <TagsTableRow key={tag.tag} tag={tag} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
|
@ -3,28 +3,26 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import { getServerId } from '../../servers/data';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import { prettify } from '../../utils/helpers/numbers';
|
||||
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
|
||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||
import { useRoutesPrefix } from '../utils/routesPrefix';
|
||||
import type { SimplifiedTag, TagModalProps } from './data';
|
||||
import { TagBullet } from './helpers/TagBullet';
|
||||
|
||||
export interface TagsTableRowProps {
|
||||
tag: SimplifiedTag;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
export const TagsTableRow = (
|
||||
DeleteTagConfirmModal: FC<TagModalProps>,
|
||||
EditTagModal: FC<TagModalProps>,
|
||||
colorGenerator: ColorGenerator,
|
||||
) => ({ tag, selectedServer }: TagsTableRowProps) => {
|
||||
) => ({ tag }: TagsTableRowProps) => {
|
||||
const [isDeleteModalOpen, toggleDelete] = useToggle();
|
||||
const [isEditModalOpen, toggleEdit] = useToggle();
|
||||
const serverId = getServerId(selectedServer);
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
|
||||
return (
|
||||
<tr className="responsive-table__row">
|
||||
|
@ -32,12 +30,12 @@ export const TagsTableRow = (
|
|||
<TagBullet tag={tag.tag} colorGenerator={colorGenerator} /> {tag.tag}
|
||||
</th>
|
||||
<td className="responsive-table__cell text-lg-end" data-th="Short URLs">
|
||||
<Link to={`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
||||
<Link to={`${routesPrefix}/list-short-urls/1?tags=${encodeURIComponent(tag.tag)}`}>
|
||||
{prettify(tag.shortUrls)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="responsive-table__cell text-lg-end" data-th="Visits">
|
||||
<Link to={`/server/${serverId}/tag/${tag.tag}/visits`}>
|
||||
<Link to={`${routesPrefix}/tag/${tag.tag}/visits`}>
|
||||
{prettify(tag.visits)}
|
||||
</Link>
|
||||
</td>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { SelectedServer } from '../../../servers/data';
|
||||
import type { Order } from '../../../utils/helpers/ordering';
|
||||
import type { SimplifiedTag } from './index';
|
||||
|
||||
|
@ -14,5 +13,4 @@ export type TagsOrder = Order<TagsOrderableFields>;
|
|||
|
||||
export interface TagsListChildrenProps {
|
||||
sortedTags: SimplifiedTag[];
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { isEmpty, reject } from 'ramda';
|
||||
import { isReachableServer } from '../../../servers/data';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
||||
import { isFeatureEnabledForVersion } from '../../utils/features';
|
||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||
import type { CreateVisit } from '../../visits/types';
|
||||
import type { TagStats } from '../data';
|
||||
|
@ -85,17 +83,13 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
|
|||
export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/listTags`,
|
||||
async (_: void, { getState }): Promise<ListTags> => {
|
||||
const { tagsList, selectedServer } = getState();
|
||||
const { tagsList } = getState();
|
||||
|
||||
if (!force && !isEmpty(tagsList.tags)) {
|
||||
return tagsList;
|
||||
}
|
||||
|
||||
const { tags, stats }: ShlinkTags = await (
|
||||
isReachableServer(selectedServer) && isFeatureEnabledForVersion('tagsStats', selectedServer.version)
|
||||
? apiClient.tagsStats()
|
||||
: apiClient.listTags()
|
||||
);
|
||||
const { tags, stats }: ShlinkTags = await apiClient.tagsStats();
|
||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
||||
acc[tag] = rest;
|
||||
return acc;
|
||||
|
|
|
@ -28,7 +28,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
bottle.serviceFactory('TagsList', TagsList, 'TagsTable');
|
||||
bottle.decorator('TagsList', connect(
|
||||
['tagsList', 'selectedServer', 'mercureInfo'],
|
||||
['tagsList', 'mercureInfo'],
|
||||
['forceListTags', 'filterTags', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
|
|
|
@ -3,12 +3,15 @@ import type { SemVer } from '../../utils/helpers/version';
|
|||
import { versionMatch } from '../../utils/helpers/version';
|
||||
|
||||
const supportedFeatures = {
|
||||
// Deprecated
|
||||
forwardQuery: '2.9.0',
|
||||
nonRestCors: '2.9.0',
|
||||
defaultDomainRedirectsEdition: '2.10.0',
|
||||
nonOrphanVisits: '3.0.0',
|
||||
allTagsFiltering: '3.0.0',
|
||||
tagsStats: '3.0.0',
|
||||
// End deprecated
|
||||
|
||||
domainVisits: '3.1.0',
|
||||
excludeBotsOnShortUrls: '3.4.0',
|
||||
filterDisabledUrls: '3.4.0',
|
||||
|
@ -23,12 +26,15 @@ export const isFeatureEnabledForVersion = (feature: Feature, serverVersion: SemV
|
|||
versionMatch(serverVersion, { minVersion: supportedFeatures[feature] });
|
||||
|
||||
const getFeaturesForVersion = (serverVersion: SemVer): Record<Feature, boolean> => ({
|
||||
// Deprecated
|
||||
forwardQuery: isFeatureEnabledForVersion('forwardQuery', serverVersion),
|
||||
nonRestCors: isFeatureEnabledForVersion('nonRestCors', serverVersion),
|
||||
defaultDomainRedirectsEdition: isFeatureEnabledForVersion('defaultDomainRedirectsEdition', serverVersion),
|
||||
nonOrphanVisits: isFeatureEnabledForVersion('nonOrphanVisits', serverVersion),
|
||||
allTagsFiltering: isFeatureEnabledForVersion('allTagsFiltering', serverVersion),
|
||||
tagsStats: isFeatureEnabledForVersion('tagsStats', serverVersion),
|
||||
// End
|
||||
|
||||
domainVisits: isFeatureEnabledForVersion('domainVisits', serverVersion),
|
||||
excludeBotsOnShortUrls: isFeatureEnabledForVersion('excludeBotsOnShortUrls', serverVersion),
|
||||
filterDisabledUrls: isFeatureEnabledForVersion('filterDisabledUrls', serverVersion),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { HighlightCardProps } from '../../../src/servers/helpers/HighlightCard';
|
||||
import { HighlightCard } from '../../../src/servers/helpers/HighlightCard';
|
||||
import type { HighlightCardProps } from '../../../src/shlink-web-component/overview/helpers/HighlightCard';
|
||||
import { HighlightCard } from '../../../src/shlink-web-component/overview/helpers/HighlightCard';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<HighlightCard />', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { VisitsHighlightCardProps } from '../../../src/servers/helpers/VisitsHighlightCard';
|
||||
import { VisitsHighlightCard } from '../../../src/servers/helpers/VisitsHighlightCard';
|
||||
import type { VisitsHighlightCardProps } from '../../../src/shlink-web-component/overview/helpers/VisitsHighlightCard';
|
||||
import { VisitsHighlightCard } from '../../../src/shlink-web-component/overview/helpers/VisitsHighlightCard';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsHighlightCard />', () => {
|
||||
|
|
Loading…
Reference in a new issue