mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Define Shlink API contract
This commit is contained in:
parent
3fe48779be
commit
5f6dc186e3
42 changed files with 161 additions and 126 deletions
|
@ -1,5 +1,5 @@
|
|||
import type { ProblemDetailsError } from './types/errors';
|
||||
import { isInvalidArgumentError } from './utils';
|
||||
import type { ProblemDetailsError } from '../shlink-web-component/api-contract';
|
||||
import { isInvalidArgumentError } from '../shlink-web-component/api-contract/utils';
|
||||
|
||||
export interface ShlinkApiErrorProps {
|
||||
errorData?: ProblemDetailsError;
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { isEmpty, isNil, reject } from 'ramda';
|
||||
import type { HttpClient } from '../../common/services/HttpClient';
|
||||
import type { ShortUrl, ShortUrlData } from '../../shlink-web-component/short-urls/data';
|
||||
import { orderToString } from '../../utils/helpers/ordering';
|
||||
import { stringifyQuery } from '../../utils/helpers/query';
|
||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
import type {
|
||||
ShlinkApiClient as BaseShlinkApiClient,
|
||||
ShlinkDomainRedirects,
|
||||
ShlinkDomainsResponse,
|
||||
ShlinkEditDomainRedirects,
|
||||
|
@ -20,9 +16,13 @@ import type {
|
|||
ShlinkTagsStatsResponse,
|
||||
ShlinkVisits,
|
||||
ShlinkVisitsOverview,
|
||||
ShlinkVisitsParams,
|
||||
} from '../types';
|
||||
import { isRegularNotFound, parseApiError } from '../utils';
|
||||
ShlinkVisitsParams } from '../../shlink-web-component/api-contract';
|
||||
import { isRegularNotFound, parseApiError } from '../../shlink-web-component/api-contract/utils';
|
||||
import type { ShortUrl, ShortUrlData } from '../../shlink-web-component/short-urls/data';
|
||||
import { orderToString } from '../../utils/helpers/ordering';
|
||||
import { stringifyQuery } from '../../utils/helpers/query';
|
||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
|
||||
type ApiVersion = 2 | 3;
|
||||
|
||||
|
@ -45,7 +45,7 @@ const normalizeListParams = (
|
|||
orderBy: orderToString(orderBy),
|
||||
});
|
||||
|
||||
export class ShlinkApiClient {
|
||||
export class ShlinkApiClient implements BaseShlinkApiClient {
|
||||
private apiVersion: ApiVersion;
|
||||
|
||||
public constructor(
|
||||
|
|
|
@ -13,6 +13,7 @@ interface MenuLayoutProps {
|
|||
settings: Settings;
|
||||
}
|
||||
|
||||
// FIXME Rename this to something else
|
||||
export const MenuLayout = (
|
||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||
ServerError: FC,
|
||||
|
|
|
@ -8,10 +8,7 @@ import { AsideMenu } from '../common/AsideMenu';
|
|||
import { NotFound } from '../common/NotFound';
|
||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||
import { useFeature } from './utils/features';
|
||||
|
||||
type MainProps = {
|
||||
routesPrefix?: string;
|
||||
};
|
||||
import { useRoutesPrefix } from './utils/routesPrefix';
|
||||
|
||||
export const Main = (
|
||||
TagsList: FC,
|
||||
|
@ -25,8 +22,9 @@ export const Main = (
|
|||
Overview: FC,
|
||||
EditShortUrl: FC,
|
||||
ManageDomains: FC,
|
||||
): FC<MainProps> => ({ routesPrefix = '' }) => {
|
||||
): FC => () => {
|
||||
const location = useLocation();
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
const [sidebarVisible, toggleSidebar, showSidebar, hideSidebar] = useToggle();
|
||||
useEffect(() => hideSidebar(), [location]);
|
||||
|
||||
|
@ -71,5 +69,3 @@ export const Main = (
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type MainType = ReturnType<typeof Main>;
|
||||
|
|
|
@ -4,8 +4,10 @@ import type { FC, ReactNode } from 'react';
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import type { SemVer } from '../utils/helpers/version';
|
||||
import type { ShlinkApiClient } from './api-contract';
|
||||
import { setUpStore } from './container/store';
|
||||
import { FeaturesProvider, useFeatures } from './utils/features';
|
||||
import { RoutesPrefixProvider } from './utils/routesPrefix';
|
||||
import type { Settings } from './utils/settings';
|
||||
import { SettingsProvider } from './utils/settings';
|
||||
|
||||
|
@ -13,7 +15,7 @@ type ShlinkWebComponentProps = {
|
|||
routesPrefix?: string;
|
||||
settings?: Settings;
|
||||
serverVersion: SemVer;
|
||||
apiClient: any;
|
||||
apiClient: ShlinkApiClient;
|
||||
};
|
||||
|
||||
export const createShlinkWebComponent = (
|
||||
|
@ -30,7 +32,7 @@ export const createShlinkWebComponent = (
|
|||
// depend on it
|
||||
const { container } = bottle;
|
||||
const { Main } = container;
|
||||
mainContent.current = <Main routesPrefix={routesPrefix} />;
|
||||
mainContent.current = <Main />;
|
||||
setStore(setUpStore(container));
|
||||
}, []);
|
||||
|
||||
|
@ -38,7 +40,9 @@ export const createShlinkWebComponent = (
|
|||
<Provider store={theStore}>
|
||||
<SettingsProvider value={settings}>
|
||||
<FeaturesProvider value={features}>
|
||||
<RoutesPrefixProvider value={routesPrefix}>
|
||||
{mainContent.current}
|
||||
</RoutesPrefixProvider>
|
||||
</FeaturesProvider>
|
||||
</SettingsProvider>
|
||||
</Provider>
|
||||
|
|
62
src/shlink-web-component/api-contract/ShlinkApiClient.ts
Normal file
62
src/shlink-web-component/api-contract/ShlinkApiClient.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import type { ShortUrl, ShortUrlData } from '../short-urls/data';
|
||||
import type {
|
||||
ShlinkDomainRedirects,
|
||||
ShlinkDomainsResponse,
|
||||
ShlinkEditDomainRedirects,
|
||||
ShlinkHealth,
|
||||
ShlinkMercureInfo,
|
||||
ShlinkShortUrlData,
|
||||
ShlinkShortUrlsListParams,
|
||||
ShlinkShortUrlsResponse,
|
||||
ShlinkTags,
|
||||
ShlinkVisits,
|
||||
ShlinkVisitsOverview,
|
||||
ShlinkVisitsParams,
|
||||
} from './types';
|
||||
|
||||
export type ShlinkApiClient = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
|
||||
listShortUrls(params?: ShlinkShortUrlsListParams): Promise<ShlinkShortUrlsResponse>;
|
||||
|
||||
createShortUrl(options: ShortUrlData): Promise<ShortUrl>;
|
||||
|
||||
getShortUrlVisits(shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits>;
|
||||
|
||||
getTagVisits(tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits>;
|
||||
|
||||
getDomainVisits(domain: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits>;
|
||||
|
||||
getOrphanVisits(query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits>;
|
||||
|
||||
getNonOrphanVisits(query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits>;
|
||||
|
||||
getVisitsOverview(): Promise<ShlinkVisitsOverview>;
|
||||
|
||||
getShortUrl(shortCode: string, domain?: string | null): Promise<ShortUrl>;
|
||||
|
||||
deleteShortUrl(shortCode: string, domain?: string | null): Promise<void>;
|
||||
|
||||
updateShortUrl(
|
||||
shortCode: string,
|
||||
domain: string | null | undefined,
|
||||
body: ShlinkShortUrlData,
|
||||
): Promise<ShortUrl>;
|
||||
|
||||
listTags(): Promise<ShlinkTags>;
|
||||
|
||||
tagsStats(): Promise<ShlinkTags>;
|
||||
|
||||
deleteTags(tags: string[]): Promise<{ tags: string[] }>;
|
||||
|
||||
editTag(oldName: string, newName: string): Promise<{ oldName: string; newName: string }>;
|
||||
|
||||
health(authority?: string): Promise<ShlinkHealth>;
|
||||
|
||||
mercureInfo(): Promise<ShlinkMercureInfo>;
|
||||
|
||||
listDomains(): Promise<ShlinkDomainsResponse>;
|
||||
|
||||
editDomainRedirects(domainRedirects: ShlinkEditDomainRedirects): Promise<ShlinkDomainRedirects>;
|
||||
};
|
3
src/shlink-web-component/api-contract/index.ts
Normal file
3
src/shlink-web-component/api-contract/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './errors';
|
||||
export * from './ShlinkApiClient';
|
||||
export * from './types';
|
|
@ -1,7 +1,7 @@
|
|||
import type { ShortUrl, ShortUrlMeta } from '../../shlink-web-component/short-urls/data';
|
||||
import type { Visit } from '../../shlink-web-component/visits/types';
|
||||
import type { Order } from '../../utils/helpers/ordering';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
import type { ShortUrl, ShortUrlMeta } from '../short-urls/data';
|
||||
import type { Visit } from '../visits/types';
|
||||
|
||||
export interface ShlinkShortUrlsResponse {
|
||||
data: ShortUrl[];
|
|
@ -2,16 +2,11 @@ import type {
|
|||
InvalidArgumentError,
|
||||
InvalidShortUrlDeletion,
|
||||
ProblemDetailsError,
|
||||
RegularNotFound } from '../types/errors';
|
||||
RegularNotFound } from './errors';
|
||||
import {
|
||||
ErrorTypeV2,
|
||||
ErrorTypeV3,
|
||||
} from '../types/errors';
|
||||
|
||||
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
|
||||
!!e && typeof e === 'object' && ['type', 'detail', 'title', 'status'].every((prop) => prop in e);
|
||||
|
||||
export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (isProblemDetails(e) ? e : undefined);
|
||||
} from './errors';
|
||||
|
||||
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
|
||||
error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT;
|
||||
|
@ -23,3 +18,8 @@ export const isInvalidDeletionError = (error?: ProblemDetailsError): error is In
|
|||
|
||||
export const isRegularNotFound = (error?: ProblemDetailsError): error is RegularNotFound =>
|
||||
(error?.type === ErrorTypeV2.NOT_FOUND || error?.type === ErrorTypeV3.NOT_FOUND) && error?.status === 404;
|
||||
|
||||
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
|
||||
!!e && typeof e === 'object' && ['type', 'detail', 'title', 'status'].every((prop) => prop in e);
|
||||
|
||||
export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (isProblemDetails(e) ? e : undefined);
|
|
@ -1,6 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkDomainRedirects } from '../../../api/types';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
||||
|
||||
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkDomainRedirects } from '../../../api/types';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../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';
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkMercureInfo } from '../../../api/types';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/mercure';
|
||||
|
||||
|
|
|
@ -107,7 +107,6 @@ export const Overview = (
|
|||
<CardBody>
|
||||
<ShortUrlsTable
|
||||
shortUrlsList={shortUrlsList}
|
||||
selectedServer={selectedServer}
|
||||
className="mb-0"
|
||||
onTagClick={(tag) => navigate(`/server/${serverId}/list-short-urls/1?tags=${encodeURIComponent(tag)}`)}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||
import type { ShlinkPaginator } from '../../api/types';
|
||||
import type {
|
||||
NumberOrEllipsis } from '../../utils/helpers/pagination';
|
||||
import {
|
||||
|
@ -9,17 +8,19 @@ import {
|
|||
prettifyPageNumber,
|
||||
progressivePagination,
|
||||
} from '../../utils/helpers/pagination';
|
||||
import type { ShlinkPaginator } from '../api-contract';
|
||||
import { useRoutesPrefix } from '../utils/routesPrefix';
|
||||
|
||||
interface PaginatorProps {
|
||||
paginator?: ShlinkPaginator;
|
||||
serverId: string;
|
||||
currentQueryString?: string;
|
||||
}
|
||||
|
||||
export const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => {
|
||||
export const Paginator = ({ paginator, currentQueryString = '' }: PaginatorProps) => {
|
||||
const { currentPage = 0, pagesCount = 0 } = paginator ?? {};
|
||||
const routesPrefix = useRoutesPrefix();
|
||||
const urlForPage = (pageNumber: NumberOrEllipsis) =>
|
||||
`/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`;
|
||||
`${routesPrefix}/list-short-urls/${pageNumber}${currentQueryString}`;
|
||||
|
||||
if (pagesCount <= 1) {
|
||||
return <div className="pb-3" />; // Return some space
|
||||
|
|
|
@ -2,13 +2,11 @@ import { pipe } from 'ramda';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Card } from 'reactstrap';
|
||||
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../../api/types';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import { getServerId } from '../../servers/data';
|
||||
import { DEFAULT_SHORT_URLS_ORDERING } from '../../settings/reducers/settings';
|
||||
import type { OrderDir } from '../../utils/helpers/ordering';
|
||||
import { determineOrderDir } from '../../utils/helpers/ordering';
|
||||
import { TableOrderIcon } from '../../utils/table/TableOrderIcon';
|
||||
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsOrder } from '../api-contract';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { useFeature } from '../utils/features';
|
||||
|
@ -21,7 +19,6 @@ import type { ShortUrlsFilteringBarType } from './ShortUrlsFilteringBar';
|
|||
import type { ShortUrlsTableType } from './ShortUrlsTable';
|
||||
|
||||
interface ShortUrlsListProps {
|
||||
selectedServer: SelectedServer;
|
||||
shortUrlsList: ShortUrlsListState;
|
||||
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
|
||||
}
|
||||
|
@ -29,8 +26,7 @@ interface ShortUrlsListProps {
|
|||
export const ShortUrlsList = (
|
||||
ShortUrlsTable: ShortUrlsTableType,
|
||||
ShortUrlsFilteringBar: ShortUrlsFilteringBarType,
|
||||
) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer }: ShortUrlsListProps) => {
|
||||
const serverId = getServerId(selectedServer);
|
||||
) => boundToMercureHub(({ listShortUrls, shortUrlsList }: ShortUrlsListProps) => {
|
||||
const { page } = useParams();
|
||||
const location = useLocation();
|
||||
const [filter, toFirstPage] = useShortUrlsQuery();
|
||||
|
@ -108,13 +104,12 @@ export const ShortUrlsList = (
|
|||
/>
|
||||
<Card body className="pb-0">
|
||||
<ShortUrlsTable
|
||||
selectedServer={selectedServer}
|
||||
shortUrlsList={shortUrlsList}
|
||||
orderByColumn={orderByColumn}
|
||||
renderOrderIcon={renderOrderIcon}
|
||||
onTagClick={addTag}
|
||||
/>
|
||||
<Paginator paginator={pagination} serverId={serverId} currentQueryString={location.search} />
|
||||
<Paginator paginator={pagination} currentQueryString={location.search} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'ramda';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { SelectedServer } from '../../servers/data';
|
||||
import type { ShortUrlsOrderableFields } from './data';
|
||||
import type { ShortUrlsRowType } from './helpers/ShortUrlsRow';
|
||||
import type { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||
|
@ -11,7 +10,6 @@ interface ShortUrlsTableProps {
|
|||
orderByColumn?: (column: ShortUrlsOrderableFields) => () => void;
|
||||
renderOrderIcon?: (column: ShortUrlsOrderableFields) => ReactNode;
|
||||
shortUrlsList: ShortUrlsListState;
|
||||
selectedServer: SelectedServer;
|
||||
onTagClick?: (tag: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -21,7 +19,6 @@ export const ShortUrlsTable = (ShortUrlsRow: ShortUrlsRowType) => ({
|
|||
renderOrderIcon,
|
||||
shortUrlsList,
|
||||
onTagClick,
|
||||
selectedServer,
|
||||
className,
|
||||
}: ShortUrlsTableProps) => {
|
||||
const { error, loading, shortUrls } = shortUrlsList;
|
||||
|
@ -52,7 +49,6 @@ export const ShortUrlsTable = (ShortUrlsRow: ShortUrlsRowType) => ({
|
|||
<ShortUrlsRow
|
||||
key={shortUrl.shortUrl}
|
||||
shortUrl={shortUrl}
|
||||
selectedServer={selectedServer}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -2,9 +2,9 @@ import { pipe } from 'ramda';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import { ShlinkApiError } from '../../../api/ShlinkApiError';
|
||||
import { isInvalidDeletionError } from '../../../api/utils';
|
||||
import { Result } from '../../../utils/Result';
|
||||
import { handleEventPreventingDefault } from '../../../utils/utils';
|
||||
import { isInvalidDeletionError } from '../../api-contract/utils';
|
||||
import type { ShortUrlIdentifier, ShortUrlModalProps } from '../data';
|
||||
import type { ShortUrlDeletion } from '../reducers/shortUrlDeletion';
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
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';
|
||||
import type { ShortUrl } from '../data';
|
||||
import { useShortUrlsQuery } from './hooks';
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { SelectedServer, ServerWithId } from '../../../servers/data';
|
||||
import { isServerWithId } from '../../../servers/data';
|
||||
import { useRoutesPrefix } from '../../utils/routesPrefix';
|
||||
import type { ShortUrl } from '../data';
|
||||
import { urlEncodeShortCode } from './index';
|
||||
|
||||
|
@ -9,21 +8,22 @@ export type LinkSuffix = 'visits' | 'edit';
|
|||
|
||||
export interface ShortUrlDetailLinkProps {
|
||||
shortUrl?: ShortUrl | null;
|
||||
selectedServer?: SelectedServer;
|
||||
suffix: LinkSuffix;
|
||||
asLink?: boolean;
|
||||
}
|
||||
|
||||
const buildUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
|
||||
const buildUrl = (routePrefix: string, { shortCode, domain }: ShortUrl, suffix: LinkSuffix) => {
|
||||
const query = domain ? `?domain=${domain}` : '';
|
||||
return `/server/${id}/short-code/${urlEncodeShortCode(shortCode)}/${suffix}${query}`;
|
||||
return `${routePrefix}/short-code/${urlEncodeShortCode(shortCode)}/${suffix}${query}`;
|
||||
};
|
||||
|
||||
export const ShortUrlDetailLink: FC<ShortUrlDetailLinkProps & Record<string | number, any>> = (
|
||||
{ selectedServer, shortUrl, suffix, children, ...rest },
|
||||
{ shortUrl, suffix, asLink, children, ...rest },
|
||||
) => {
|
||||
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
|
||||
const routePrefix = useRoutesPrefix();
|
||||
if (!asLink || !shortUrl) {
|
||||
return <span {...rest}>{children}</span>;
|
||||
}
|
||||
|
||||
return <Link to={buildUrl(selectedServer, shortUrl, suffix)} {...rest}>{children}</Link>;
|
||||
return <Link to={buildUrl(routePrefix, shortUrl, suffix)} {...rest}>{children}</Link>;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import type { SelectedServer } from '../../../servers/data';
|
||||
import { formatHumanFriendly, parseISO } from '../../../utils/helpers/date';
|
||||
import { useElementRef } from '../../../utils/helpers/hooks';
|
||||
import { prettify } from '../../../utils/helpers/numbers';
|
||||
|
@ -12,18 +11,18 @@ import './ShortUrlVisitsCount.scss';
|
|||
|
||||
interface ShortUrlVisitsCountProps {
|
||||
shortUrl?: ShortUrl | null;
|
||||
selectedServer?: SelectedServer;
|
||||
visitsCount: number;
|
||||
active?: boolean;
|
||||
asLink?: boolean;
|
||||
}
|
||||
|
||||
export const ShortUrlVisitsCount = (
|
||||
{ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCountProps,
|
||||
{ visitsCount, shortUrl, active = false, asLink = false }: ShortUrlVisitsCountProps,
|
||||
) => {
|
||||
const { maxVisits, validSince, validUntil } = shortUrl?.meta ?? {};
|
||||
const hasLimit = !!maxVisits || !!validSince || !!validUntil;
|
||||
const visitsLink = (
|
||||
<ShortUrlDetailLink selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||
<ShortUrlDetailLink shortUrl={shortUrl} suffix="visits" asLink={asLink}>
|
||||
<strong
|
||||
className={classNames('short-url-visits-count__amount', { 'short-url-visits-count__amount--big': active })}
|
||||
>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { FC } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import type { SelectedServer } from '../../../servers/data';
|
||||
import { CopyToClipboardIcon } from '../../../utils/CopyToClipboardIcon';
|
||||
import { Time } from '../../../utils/dates/Time';
|
||||
import type { TimeoutToggle } from '../../../utils/helpers/hooks';
|
||||
|
@ -17,7 +16,6 @@ import './ShortUrlsRow.scss';
|
|||
|
||||
interface ShortUrlsRowProps {
|
||||
onTagClick?: (tag: string) => void;
|
||||
selectedServer: SelectedServer;
|
||||
shortUrl: ShortUrl;
|
||||
}
|
||||
|
||||
|
@ -27,7 +25,7 @@ export const ShortUrlsRow = (
|
|||
ShortUrlsRowMenu: ShortUrlsRowMenuType,
|
||||
colorGenerator: ColorGenerator,
|
||||
useTimeoutToggle: TimeoutToggle,
|
||||
) => ({ shortUrl, selectedServer, onTagClick }: ShortUrlsRowProps) => {
|
||||
) => ({ shortUrl, onTagClick }: ShortUrlsRowProps) => {
|
||||
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
|
||||
const [active, setActive] = useTimeoutToggle(false, 500);
|
||||
const isFirstRun = useRef(true);
|
||||
|
@ -76,15 +74,15 @@ export const ShortUrlsRow = (
|
|||
doExcludeBots ? shortUrl.visitsSummary?.nonBots : shortUrl.visitsSummary?.total
|
||||
) ?? shortUrl.visitsCount}
|
||||
shortUrl={shortUrl}
|
||||
selectedServer={selectedServer}
|
||||
active={active}
|
||||
asLink
|
||||
/>
|
||||
</td>
|
||||
<td className="responsive-table__cell short-urls-row__cell" data-th="Status">
|
||||
<ShortUrlStatus shortUrl={shortUrl} />
|
||||
</td>
|
||||
<td className="responsive-table__cell short-urls-row__cell text-end">
|
||||
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />
|
||||
<ShortUrlsRowMenu shortUrl={shortUrl} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -7,14 +7,12 @@ import {
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import type { SelectedServer } from '../../../servers/data';
|
||||
import { useToggle } from '../../../utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../../../utils/RowDropdownBtn';
|
||||
import type { ShortUrl, ShortUrlModalProps } from '../data';
|
||||
import { ShortUrlDetailLink } from './ShortUrlDetailLink';
|
||||
|
||||
interface ShortUrlsRowMenuProps {
|
||||
selectedServer: SelectedServer;
|
||||
shortUrl: ShortUrl;
|
||||
}
|
||||
type ShortUrlModal = FC<ShortUrlModalProps>;
|
||||
|
@ -22,17 +20,17 @@ type ShortUrlModal = FC<ShortUrlModalProps>;
|
|||
export const ShortUrlsRowMenu = (
|
||||
DeleteShortUrlModal: ShortUrlModal,
|
||||
QrCodeModal: ShortUrlModal,
|
||||
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
||||
) => ({ shortUrl }: ShortUrlsRowMenuProps) => {
|
||||
const [isQrModalOpen,, openQrCodeModal, closeQrCodeModal] = useToggle();
|
||||
const [isDeleteModalOpen,, openDeleteModal, closeDeleteModal] = useToggle();
|
||||
|
||||
return (
|
||||
<RowDropdownBtn minWidth={190}>
|
||||
<DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
|
||||
<DropdownItem tag={ShortUrlDetailLink} shortUrl={shortUrl} suffix="visits">
|
||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="edit">
|
||||
<DropdownItem tag={ShortUrlDetailLink} shortUrl={shortUrl} suffix="edit">
|
||||
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit short URL
|
||||
</DropdownItem>
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { ShortUrl, ShortUrlData } from '../data';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlCreation';
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
||||
import { shortUrlMatches } from '../helpers';
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { EditShortUrlData, ShortUrl, ShortUrlIdentifier } from '../data';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlEdition';
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { assocPath, last, pipe, reject } from 'ramda';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../../api/types';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api-contract';
|
||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||
import type { ShortUrl } from '../data';
|
||||
import { shortUrlMatches } from '../helpers';
|
||||
|
@ -26,15 +25,7 @@ const initialState: ShortUrlsList = {
|
|||
|
||||
export const listShortUrls = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/listShortUrls`,
|
||||
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => {
|
||||
try {
|
||||
const { listShortUrls: shlinkListShortUrls } = apiClient;
|
||||
return shlinkListShortUrls(params ?? {});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => apiClient.listShortUrls(params ?? {}),
|
||||
);
|
||||
|
||||
export const shortUrlsListReducerCreator = (
|
||||
|
|
|
@ -23,7 +23,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
// Components
|
||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'ShortUrlsFilteringBar');
|
||||
bottle.decorator('ShortUrlsList', connect(
|
||||
['selectedServer', 'mercureInfo', 'shortUrlsList'],
|
||||
['mercureInfo', 'shortUrlsList'],
|
||||
['listShortUrls', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/tagDelete';
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { pick } from 'ramda';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ColorGenerator } from '../../../utils/services/ColorGenerator';
|
||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/tagEdit';
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { isEmpty, reject } from 'ramda';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkTags } from '../../../api/types';
|
||||
import type { ProblemDetailsError } from '../../../api/types/errors';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
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';
|
||||
|
|
7
src/shlink-web-component/utils/routesPrefix.ts
Normal file
7
src/shlink-web-component/utils/routesPrefix.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
const RoutesPrefixContext = createContext('');
|
||||
|
||||
export const RoutesPrefixProvider = RoutesPrefixContext.Provider;
|
||||
|
||||
export const useRoutesPrefix = (): string => useContext(RoutesPrefixContext);
|
|
@ -1,7 +1,7 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import type { ShlinkVisitsParams } from '../../api/types';
|
||||
import type { ReportExporter } from '../../common/services/ReportExporter';
|
||||
import { useGoBack } from '../../utils/helpers/hooks';
|
||||
import type { ShlinkVisitsParams } from '../api-contract';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||
import type { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../../api/types';
|
||||
import { parseApiError } from '../../../api/utils';
|
||||
import type { ShlinkState } from '../../../container/types';
|
||||
import type { DateInterval } from '../../../utils/helpers/dateIntervals';
|
||||
import { dateToMatchingInterval } from '../../../utils/helpers/dateIntervals';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../api-contract';
|
||||
import { parseApiError } from '../../api-contract/utils';
|
||||
import type { CreateVisit, Visit } from '../types';
|
||||
import type { LoadVisits, VisitsInfo, VisitsLoaded } from './types';
|
||||
import { createNewVisits } from './visitCreation';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import { isBetween } from '../../../utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { domainMatches } from '../../short-urls/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import { isBetween } from '../../../utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { VisitsInfo } from './types';
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkApiClientBuilder } from '../../../api/services/ShlinkApiClientBuilder';
|
||||
import { isBetween } from '../../../utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import type { OrphanVisit, OrphanVisitType } from '../types';
|
||||
import { isOrphanVisit } from '../types/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import { isBetween } from '../../../utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import type { ShortUrlIdentifier } from '../../short-urls/data';
|
||||
import { shortUrlMatches } from '../../short-urls/helpers';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import { isBetween } from '../../../utils/helpers/date';
|
||||
import type { ShlinkApiClient } from '../../api-contract';
|
||||
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
|
||||
import type { LoadVisits, VisitsInfo } from './types';
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { ShlinkVisitsParams } from '../../../../api/types';
|
||||
import type { ProblemDetailsError } from '../../../../api/types/errors';
|
||||
import type { DateInterval } from '../../../../utils/helpers/dateIntervals';
|
||||
import type { ProblemDetailsError, ShlinkVisitsParams } from '../../../api-contract';
|
||||
import type { Visit } from '../../types';
|
||||
|
||||
export interface VisitsInfo {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient';
|
||||
import type { ShlinkVisitsOverview } from '../../../api/types';
|
||||
import { createAsyncThunk } from '../../../utils/helpers/redux';
|
||||
import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../api-contract';
|
||||
import type { CreateVisit } from '../types';
|
||||
import { groupNewVisitsByType } from '../types/helpers';
|
||||
import { createNewVisits } from './visitCreation';
|
||||
|
|
Loading…
Reference in a new issue