Define Shlink API contract

This commit is contained in:
Alejandro Celaya 2023-07-24 17:30:58 +02:00
parent 3fe48779be
commit 5f6dc186e3
42 changed files with 161 additions and 126 deletions

View file

@ -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;

View file

@ -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(

View file

@ -13,6 +13,7 @@ interface MenuLayoutProps {
settings: Settings;
}
// FIXME Rename this to something else
export const MenuLayout = (
buildShlinkApiClient: ShlinkApiClientBuilder,
ServerError: FC,

View file

@ -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>;

View file

@ -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}>
{mainContent.current}
<RoutesPrefixProvider value={routesPrefix}>
{mainContent.current}
</RoutesPrefixProvider>
</FeaturesProvider>
</SettingsProvider>
</Provider>

View 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>;
};

View file

@ -0,0 +1,3 @@
export * from './errors';
export * from './ShlinkApiClient';
export * from './types';

View file

@ -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[];

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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)}`)}
/>

View file

@ -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

View file

@ -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>
</>
);

View file

@ -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}
/>
));

View file

@ -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';

View file

@ -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';

View file

@ -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>;
};

View file

@ -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 })}
>

View file

@ -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>
);

View file

@ -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>

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 = (

View file

@ -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'],
));

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -0,0 +1,7 @@
import { createContext, useContext } from 'react';
const RoutesPrefixContext = createContext('');
export const RoutesPrefixProvider = RoutesPrefixContext.Provider;
export const useRoutesPrefix = (): string => useContext(RoutesPrefixContext);

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 {

View file

@ -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';