Move all relevant API types to api-contract

This commit is contained in:
Alejandro Celaya 2023-08-07 14:02:36 +02:00
parent d97db9e17c
commit f16f51a2b3
10 changed files with 66 additions and 61 deletions

View file

@ -1,12 +1,12 @@
import type { ShortUrlData } from '../short-urls/data';
import type { import type {
ShlinkCreateShortUrlData,
ShlinkDomainRedirects, ShlinkDomainRedirects,
ShlinkDomainsResponse, ShlinkDomainsResponse,
ShlinkEditDomainRedirects, ShlinkEditDomainRedirects,
ShlinkEditShortUrlData,
ShlinkHealth, ShlinkHealth,
ShlinkMercureInfo, ShlinkMercureInfo,
ShlinkShortUrl, ShlinkShortUrl,
ShlinkShortUrlData,
ShlinkShortUrlsListParams, ShlinkShortUrlsListParams,
ShlinkShortUrlsResponse, ShlinkShortUrlsResponse,
ShlinkTags, ShlinkTags,
@ -21,7 +21,7 @@ export type ShlinkApiClient = {
listShortUrls(params?: ShlinkShortUrlsListParams): Promise<ShlinkShortUrlsResponse>; listShortUrls(params?: ShlinkShortUrlsListParams): Promise<ShlinkShortUrlsResponse>;
createShortUrl(options: ShortUrlData): Promise<ShlinkShortUrl>; createShortUrl(options: ShlinkCreateShortUrlData): Promise<ShlinkShortUrl>;
getShortUrlVisits(shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits>; getShortUrlVisits(shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits>;
@ -42,7 +42,7 @@ export type ShlinkApiClient = {
updateShortUrl( updateShortUrl(
shortCode: string, shortCode: string,
domain: string | null | undefined, domain: string | null | undefined,
body: ShlinkShortUrlData, body: ShlinkEditShortUrlData,
): Promise<ShlinkShortUrl>; ): Promise<ShlinkShortUrl>;
listTags(): Promise<ShlinkTags>; listTags(): Promise<ShlinkTags>;

View file

@ -31,6 +31,34 @@ export interface ShlinkShortUrl {
forwardQuery?: boolean; forwardQuery?: boolean;
} }
export interface ShlinkEditShortUrlData {
longUrl?: string;
title?: string | null;
tags?: string[];
deviceLongUrls?: ShlinkDeviceLongUrls;
crawlable?: boolean;
forwardQuery?: boolean;
validSince?: string | null;
validUntil?: string | null;
maxVisits?: number | null;
/** @deprecated */
validateUrl?: boolean;
}
export interface ShlinkCreateShortUrlData extends Omit<ShlinkEditShortUrlData, 'deviceLongUrls'> {
longUrl: string;
customSlug?: string;
shortCodeLength?: number;
domain?: string;
findIfExists?: boolean;
deviceLongUrls?: {
android?: string;
ios?: string;
desktop?: string;
}
}
export interface ShlinkShortUrlsResponse { export interface ShlinkShortUrlsResponse {
data: ShlinkShortUrl[]; data: ShlinkShortUrl[];
pagination: ShlinkPaginator; pagination: ShlinkPaginator;
@ -106,20 +134,6 @@ export interface ShlinkVisitsParams {
excludeBots?: boolean; excludeBots?: boolean;
} }
export interface ShlinkShortUrlData {
longUrl?: string;
title?: string | null;
/** @deprecated */
validateUrl?: boolean;
tags?: string[];
deviceLongUrls?: ShlinkDeviceLongUrls;
crawlable?: boolean;
forwardQuery?: boolean;
validSince?: string | null;
validUntil?: string | null;
maxVisits?: number | null;
}
export interface ShlinkDomainRedirects { export interface ShlinkDomainRedirects {
baseUrlRedirect: string | null; baseUrlRedirect: string | null;
regular404Redirect: string | null; regular404Redirect: string | null;

View file

@ -1,8 +1,8 @@
import type { ShlinkCreateShortUrlData } from '@shlinkio/shlink-web-component/api-contract';
import type { FC } from 'react'; import type { FC } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { ShortUrlCreationSettings } from '../utils/settings'; import type { ShortUrlCreationSettings } from '../utils/settings';
import { useSetting } from '../utils/settings'; import { useSetting } from '../utils/settings';
import type { ShortUrlData } from './data';
import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult'; import type { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
import type { ShortUrlCreation } from './reducers/shortUrlCreation'; import type { ShortUrlCreation } from './reducers/shortUrlCreation';
import type { ShortUrlFormProps } from './ShortUrlForm'; import type { ShortUrlFormProps } from './ShortUrlForm';
@ -13,11 +13,11 @@ export interface CreateShortUrlProps {
interface CreateShortUrlConnectProps extends CreateShortUrlProps { interface CreateShortUrlConnectProps extends CreateShortUrlProps {
shortUrlCreation: ShortUrlCreation; shortUrlCreation: ShortUrlCreation;
createShortUrl: (data: ShortUrlData) => Promise<void>; createShortUrl: (data: ShlinkCreateShortUrlData) => Promise<void>;
resetCreateShortUrl: () => void; resetCreateShortUrl: () => void;
} }
const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ({ const getInitialState = (settings?: ShortUrlCreationSettings): ShlinkCreateShortUrlData => ({
longUrl: '', longUrl: '',
tags: [], tags: [],
customSlug: '', customSlug: '',
@ -33,7 +33,7 @@ const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => (
}); });
export const CreateShortUrl = ( export const CreateShortUrl = (
ShortUrlForm: FC<ShortUrlFormProps>, ShortUrlForm: FC<ShortUrlFormProps<ShlinkCreateShortUrlData>>,
CreateShortUrlResult: FC<CreateShortUrlResultProps>, CreateShortUrlResult: FC<CreateShortUrlResultProps>,
) => ({ ) => ({
createShortUrl, createShortUrl,
@ -50,7 +50,7 @@ export const CreateShortUrl = (
initialState={initialState} initialState={initialState}
saving={shortUrlCreation.saving} saving={shortUrlCreation.saving}
mode={basicMode ? 'create-basic' : 'create'} mode={basicMode ? 'create-basic' : 'create'}
onSave={async (data: ShortUrlData) => { onSave={async (data) => {
resetCreateShortUrl(); resetCreateShortUrl();
return createShortUrl(data); return createShortUrl(data);
}} }}

View file

@ -1,6 +1,7 @@
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Message, parseQuery, Result } from '@shlinkio/shlink-frontend-kit'; import { Message, parseQuery, Result } from '@shlinkio/shlink-frontend-kit';
import type { ShlinkEditShortUrlData } from '@shlinkio/shlink-web-component/api-contract';
import type { FC } from 'react'; import type { FC } from 'react';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
@ -22,7 +23,7 @@ interface EditShortUrlConnectProps {
editShortUrl: (editShortUrl: EditShortUrlInfo) => void; editShortUrl: (editShortUrl: EditShortUrlInfo) => void;
} }
export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps>) => ({ export const EditShortUrl = (ShortUrlForm: FC<ShortUrlFormProps<ShlinkEditShortUrlData>>) => ({
shortUrlDetail, shortUrlDetail,
getShortUrlDetail, getShortUrlDetail,
shortUrlEdition, shortUrlEdition,

View file

@ -9,7 +9,7 @@ import type { ChangeEvent, FC } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Button, FormGroup, Input, Row } from 'reactstrap'; import { Button, FormGroup, Input, Row } from 'reactstrap';
import type { InputType } from 'reactstrap/types/lib/Input'; import type { InputType } from 'reactstrap/types/lib/Input';
import type { ShlinkDeviceLongUrls } from '../api-contract'; import type { ShlinkCreateShortUrlData, ShlinkDeviceLongUrls, ShlinkEditShortUrlData } from '../api-contract';
import type { DomainSelectorProps } from '../domains/DomainSelector'; import type { DomainSelectorProps } from '../domains/DomainSelector';
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
import { IconInput } from '../utils/components/IconInput'; import { IconInput } from '../utils/components/IconInput';
@ -18,7 +18,6 @@ import { DateTimeInput } from '../utils/dates/DateTimeInput';
import { formatIsoDate } from '../utils/dates/helpers/date'; import { formatIsoDate } from '../utils/dates/helpers/date';
import { useFeature } from '../utils/features'; import { useFeature } from '../utils/features';
import { handleEventPreventingDefault, hasValue } from '../utils/helpers'; import { handleEventPreventingDefault, hasValue } from '../utils/helpers';
import type { ShortUrlData } from './data';
import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup'; import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon'; import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon';
import './ShortUrlForm.scss'; import './ShortUrlForm.scss';
@ -28,25 +27,32 @@ export type Mode = 'create' | 'create-basic' | 'edit';
type DateFields = 'validSince' | 'validUntil'; type DateFields = 'validSince' | 'validUntil';
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title'; type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title';
export interface ShortUrlFormProps { export interface ShortUrlFormProps<T extends ShlinkCreateShortUrlData | ShlinkEditShortUrlData> {
// FIXME Try to get rid of the mode param, and infer creation or edition from initialState if possible
mode: Mode; mode: Mode;
saving: boolean; saving: boolean;
initialState: ShortUrlData; initialState: T;
onSave: (shortUrlData: ShortUrlData) => Promise<unknown>; onSave: (shortUrlData: T) => Promise<unknown>;
} }
const normalizeTag = pipe(trim, replace(/ /g, '-')); const normalizeTag = pipe(trim, replace(/ /g, '-'));
const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date); const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
const isCreationData = (data: ShlinkCreateShortUrlData | ShlinkEditShortUrlData): data is ShlinkCreateShortUrlData =>
'shortCodeLength' in data && 'customSlug' in data && 'domain' in data;
export const ShortUrlForm = ( export const ShortUrlForm = (
TagsSelector: FC<TagsSelectorProps>, TagsSelector: FC<TagsSelectorProps>,
DomainSelector: FC<DomainSelectorProps>, DomainSelector: FC<DomainSelectorProps>,
): FC<ShortUrlFormProps> => ({ mode, saving, onSave, initialState }) => { ) => function ShortUrlFormComp<T extends ShlinkCreateShortUrlData | ShlinkEditShortUrlData>(
{ mode, saving, onSave, initialState }: ShortUrlFormProps<T>,
) {
const [shortUrlData, setShortUrlData] = useState(initialState); const [shortUrlData, setShortUrlData] = useState(initialState);
const reset = () => setShortUrlData(initialState); const reset = () => setShortUrlData(initialState);
const supportsDeviceLongUrls = useFeature('deviceLongUrls'); const supportsDeviceLongUrls = useFeature('deviceLongUrls');
const isEdit = mode === 'edit'; const isEdit = mode === 'edit';
const isCreation = isCreationData(shortUrlData);
const isBasicMode = mode === 'create-basic'; const isBasicMode = mode === 'create-basic';
const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) }); const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) });
const setResettableValue = (value: string, initialValue?: any) => { const setResettableValue = (value: string, initialValue?: any) => {
@ -82,6 +88,7 @@ export const ShortUrlForm = (
id={id} id={id}
type={type} type={type}
placeholder={placeholder} placeholder={placeholder}
// @ts-expect-error FIXME Make sure id is a key from T
value={shortUrlData[id] ?? ''} value={shortUrlData[id] ?? ''}
onChange={props.onChange ?? ((e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value }))} onChange={props.onChange ?? ((e) => setShortUrlData({ ...shortUrlData, [id]: e.target.value }))}
{...props} {...props}
@ -171,7 +178,7 @@ export const ShortUrlForm = (
title: setResettableValue(target.value, initialState.title), title: setResettableValue(target.value, initialState.title),
}), }),
})} })}
{!isEdit && ( {!isEdit && isCreation && (
<> <>
<Row> <Row>
<div className="col-lg-6"> <div className="col-lg-6">
@ -216,7 +223,7 @@ export const ShortUrlForm = (
> >
Validate URL Validate URL
</ShortUrlFormCheckboxGroup> </ShortUrlFormCheckboxGroup>
{!isEdit && ( {!isEdit && isCreation && (
<p> <p>
<Checkbox <Checkbox
inline inline

View file

@ -1,20 +1,7 @@
import type { Order } from '@shlinkio/shlink-frontend-kit'; import type { Order } from '@shlinkio/shlink-frontend-kit';
import type { ShlinkShortUrl, ShlinkShortUrlData } from '../../api-contract'; import type { ShlinkShortUrl } from '../../api-contract';
import type { OptionalString } from '../../utils/helpers'; import type { OptionalString } from '../../utils/helpers';
export interface ShortUrlData extends Omit<ShlinkShortUrlData, 'deviceLongUrls'> {
longUrl: string;
customSlug?: string;
shortCodeLength?: number;
domain?: string;
findIfExists?: boolean;
deviceLongUrls?: {
android?: string;
ios?: string;
desktop?: string;
}
}
export interface ShortUrlIdentifier { export interface ShortUrlIdentifier {
shortCode: string; shortCode: string;
domain?: OptionalString; domain?: OptionalString;

View file

@ -1,9 +1,8 @@
import { isNil } from 'ramda'; import { isNil } from 'ramda';
import type { ShlinkShortUrl } from '../../api-contract'; import type { ShlinkCreateShortUrlData, ShlinkShortUrl } from '../../api-contract';
import type { OptionalString } from '../../utils/helpers'; import type { OptionalString } from '../../utils/helpers';
import type { ShortUrlCreationSettings } from '../../utils/settings'; import type { ShortUrlCreationSettings } from '../../utils/settings';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { ShortUrlData } from '../data';
export const shortUrlMatches = (shortUrl: ShlinkShortUrl, shortCode: string, domain: OptionalString): boolean => { export const shortUrlMatches = (shortUrl: ShlinkShortUrl, shortCode: string, domain: OptionalString): boolean => {
if (isNil(domain)) { if (isNil(domain)) {
@ -21,10 +20,11 @@ export const domainMatches = (shortUrl: ShlinkShortUrl, domain: string): boolean
return shortUrl.domain === domain; return shortUrl.domain === domain;
}; };
// FIXME This should return ShlinkEditShortUrlData
export const shortUrlDataFromShortUrl = ( export const shortUrlDataFromShortUrl = (
shortUrl?: ShlinkShortUrl, shortUrl?: ShlinkShortUrl,
settings?: ShortUrlCreationSettings, settings?: ShortUrlCreationSettings,
): ShortUrlData => { ): ShlinkCreateShortUrlData => {
const validateUrl = settings?.validateUrls ?? false; const validateUrl = settings?.validateUrls ?? false;
if (!shortUrl) { if (!shortUrl) {

View file

@ -1,9 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { ProblemDetailsError, ShlinkApiClient, ShlinkShortUrl } from '../../api-contract'; import type { ProblemDetailsError, ShlinkApiClient, ShlinkCreateShortUrlData, ShlinkShortUrl } from '../../api-contract';
import { parseApiError } from '../../api-contract/utils'; import { parseApiError } from '../../api-contract/utils';
import { createAsyncThunk } from '../../utils/redux'; import { createAsyncThunk } from '../../utils/redux';
import type { ShortUrlData } from '../data';
const REDUCER_PREFIX = 'shlink/shortUrlCreation'; const REDUCER_PREFIX = 'shlink/shortUrlCreation';
@ -27,8 +25,6 @@ export type ShortUrlCreation = {
error: false; error: false;
}; };
export type CreateShortUrlAction = PayloadAction<ShlinkShortUrl>;
const initialState: ShortUrlCreation = { const initialState: ShortUrlCreation = {
saving: false, saving: false,
saved: false, saved: false,
@ -37,7 +33,7 @@ const initialState: ShortUrlCreation = {
export const createShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( export const createShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
`${REDUCER_PREFIX}/createShortUrl`, `${REDUCER_PREFIX}/createShortUrl`,
(data: ShortUrlData): Promise<ShlinkShortUrl> => apiClientFactory().createShortUrl(data), (data: ShlinkCreateShortUrlData): Promise<ShlinkShortUrl> => apiClientFactory().createShortUrl(data),
); );
export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => { export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => {

View file

@ -1,5 +1,5 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { ProblemDetailsError, ShlinkApiClient, ShlinkShortUrl, ShlinkShortUrlData } from '../../api-contract'; import type { ProblemDetailsError, ShlinkApiClient, ShlinkEditShortUrlData, ShlinkShortUrl } from '../../api-contract';
import { parseApiError } from '../../api-contract/utils'; import { parseApiError } from '../../api-contract/utils';
import { createAsyncThunk } from '../../utils/redux'; import { createAsyncThunk } from '../../utils/redux';
import type { ShortUrlIdentifier } from '../data'; import type { ShortUrlIdentifier } from '../data';
@ -15,7 +15,7 @@ export interface ShortUrlEdition {
} }
export interface EditShortUrl extends ShortUrlIdentifier { export interface EditShortUrl extends ShortUrlIdentifier {
data: ShlinkShortUrlData; data: ShlinkEditShortUrlData;
} }
const initialState: ShortUrlEdition = { const initialState: ShortUrlEdition = {

View file

@ -2,13 +2,14 @@ import { orderToString, stringifyQuery } from '@shlinkio/shlink-frontend-kit';
import type { import type {
RegularNotFound, RegularNotFound,
ShlinkApiClient as BaseShlinkApiClient, ShlinkApiClient as BaseShlinkApiClient,
ShlinkCreateShortUrlData,
ShlinkDomainRedirects, ShlinkDomainRedirects,
ShlinkDomainsResponse, ShlinkDomainsResponse,
ShlinkEditDomainRedirects, ShlinkEditDomainRedirects,
ShlinkEditShortUrlData,
ShlinkHealth, ShlinkHealth,
ShlinkMercureInfo, ShlinkMercureInfo,
ShlinkShortUrl, ShlinkShortUrl,
ShlinkShortUrlData,
ShlinkShortUrlsListNormalizedParams, ShlinkShortUrlsListNormalizedParams,
ShlinkShortUrlsListParams, ShlinkShortUrlsListParams,
ShlinkShortUrlsResponse, ShlinkShortUrlsResponse,
@ -24,7 +25,6 @@ import {
ErrorTypeV3, ErrorTypeV3,
} from '@shlinkio/shlink-web-component/api-contract'; } from '@shlinkio/shlink-web-component/api-contract';
import { isEmpty, isNil, reject } from 'ramda'; import { isEmpty, isNil, reject } from 'ramda';
import type { ShortUrlData } from '../../../shlink-web-component/src/short-urls/data';
import type { HttpClient } from '../../common/services/HttpClient'; import type { HttpClient } from '../../common/services/HttpClient';
import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
import type { OptionalString } from '../../utils/utils'; import type { OptionalString } from '../../utils/utils';
@ -73,7 +73,7 @@ export class ShlinkApiClient implements BaseShlinkApiClient {
{ url: '/short-urls', query: normalizeListParams(params) }, { url: '/short-urls', query: normalizeListParams(params) },
).then(({ shortUrls }) => shortUrls); ).then(({ shortUrls }) => shortUrls);
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShlinkShortUrl> => { public readonly createShortUrl = async (options: ShlinkCreateShortUrlData): Promise<ShlinkShortUrl> => {
const body = reject((value) => isEmpty(value) || isNil(value), options as any); const body = reject((value) => isEmpty(value) || isNil(value), options as any);
return this.performRequest<ShlinkShortUrl>({ url: '/short-urls', method: 'POST', body }); return this.performRequest<ShlinkShortUrl>({ url: '/short-urls', method: 'POST', body });
}; };
@ -106,7 +106,7 @@ export class ShlinkApiClient implements BaseShlinkApiClient {
public readonly updateShortUrl = async ( public readonly updateShortUrl = async (
shortCode: string, shortCode: string,
domain: OptionalString, domain: OptionalString,
body: ShlinkShortUrlData, body: ShlinkEditShortUrlData,
): Promise<ShlinkShortUrl> => ): Promise<ShlinkShortUrl> =>
this.performRequest<ShlinkShortUrl>({ url: `/short-urls/${shortCode}`, method: 'PATCH', query: { domain }, body }); this.performRequest<ShlinkShortUrl>({ url: `/short-urls/${shortCode}`, method: 'PATCH', query: { domain }, body });