mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 09:47:28 +03:00
Merge pull request #361 from acelaya-forks/feature/errors-improvements
Feature/errors improvements
This commit is contained in:
commit
9fdfdf865e
30 changed files with 210 additions and 105 deletions
|
@ -24,7 +24,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
### Changed
|
### Changed
|
||||||
* [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
|
* [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
|
||||||
* [#352](https://github.com/shlinkio/shlink-web-client/issues/352) Moved from Scrutinizer to Codecov as the code coverage backend.
|
* [#352](https://github.com/shlinkio/shlink-web-client/issues/352) Moved from Scrutinizer to Codecov as the code coverage backend.
|
||||||
* [#217](https://github.com/shlinkio/shlink-web-client/issues/217) Improved how messages are displayed, by centralizing it in the `Message` and `Result` components..
|
* [#217](https://github.com/shlinkio/shlink-web-client/issues/217) Improved how messages are displayed, by centralizing it in the `Message` and `Result` components.
|
||||||
|
* [#219](https://github.com/shlinkio/shlink-web-client/issues/219) Improved error messages when something fails while interacting with Shlink's API.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
15
src/api/ShlinkApiError.tsx
Normal file
15
src/api/ShlinkApiError.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { isInvalidArgumentError, ProblemDetailsError } from '../utils/services/types';
|
||||||
|
|
||||||
|
interface ShlinkApiErrorProps {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
fallbackMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShlinkApiError = ({ errorData, fallbackMessage }: ShlinkApiErrorProps) => (
|
||||||
|
<>
|
||||||
|
{errorData?.detail ?? fallbackMessage}
|
||||||
|
{isInvalidArgumentError(errorData) &&
|
||||||
|
<p className="mb-0">Invalid elements: [{errorData.invalidElements.join(', ')}]</p>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
4
src/api/util.ts
Normal file
4
src/api/util.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ProblemDetailsError } from '../utils/services/types';
|
||||||
|
|
||||||
|
export const parseApiError = (e: AxiosError<ProblemDetailsError>) => e.response?.data;
|
|
@ -9,6 +9,7 @@ import { ShortUrlCreation } from '../reducers/shortUrlCreation';
|
||||||
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
import { StateFlagTimeout } from '../../utils/helpers/hooks';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
import './CreateShortUrlResult.scss';
|
import './CreateShortUrlResult.scss';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
||||||
resetCreateShortUrl: () => void;
|
resetCreateShortUrl: () => void;
|
||||||
|
@ -16,7 +17,7 @@ export interface CreateShortUrlResultProps extends ShortUrlCreation {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||||
{ error, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
{ error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps,
|
||||||
) => {
|
) => {
|
||||||
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout();
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
|
||||||
return (
|
return (
|
||||||
<Result type="error" className="mt-3">
|
<Result type="error" className="mt-3">
|
||||||
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
|
||||||
An error occurred while creating the URL :(
|
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while creating the URL :(" />
|
||||||
</Result>
|
</Result>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { ShortUrlDeletion } from '../reducers/shortUrlDeletion';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
import { handleEventPreventingDefault, OptionalString } from '../../utils/utils';
|
import { handleEventPreventingDefault, OptionalString } from '../../utils/utils';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { isInvalidDeletionError } from '../../utils/services/types';
|
||||||
const THRESHOLD_REACHED = 'INVALID_SHORTCODE_DELETION';
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
|
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
|
||||||
shortUrlDeletion: ShortUrlDeletion;
|
shortUrlDeletion: ShortUrlDeletion;
|
||||||
|
@ -22,9 +22,6 @@ const DeleteShortUrlModal = (
|
||||||
useEffect(() => resetDeleteShortUrl, []);
|
useEffect(() => resetDeleteShortUrl, []);
|
||||||
|
|
||||||
const { error, errorData } = shortUrlDeletion;
|
const { error, errorData } = shortUrlDeletion;
|
||||||
const errorCode = error && errorData?.type;
|
|
||||||
const hasThresholdError = errorCode === THRESHOLD_REACHED;
|
|
||||||
const hasErrorOtherThanThreshold = error && errorCode !== THRESHOLD_REACHED;
|
|
||||||
const close = pipe(resetDeleteShortUrl, toggle);
|
const close = pipe(resetDeleteShortUrl, toggle);
|
||||||
const handleDeleteUrl = handleEventPreventingDefault(() => {
|
const handleDeleteUrl = handleEventPreventingDefault(() => {
|
||||||
const { shortCode, domain } = shortUrl;
|
const { shortCode, domain } = shortUrl;
|
||||||
|
@ -53,15 +50,9 @@ const DeleteShortUrlModal = (
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasThresholdError && (
|
{error && (
|
||||||
<Result type="warning" small className="mt-2">
|
<Result type={isInvalidDeletionError(errorData) ? 'warning' : 'error'} small className="mt-2">
|
||||||
{errorData?.threshold && `This short URL has received more than ${errorData.threshold} visits, and therefore, it cannot be deleted.`}
|
<ShlinkApiError errorData={errorData} fallbackMessage="Something went wrong while deleting the URL :(" />
|
||||||
{!errorData?.threshold && 'This short URL has received too many visits, and therefore, it cannot be deleted.'}
|
|
||||||
</Result>
|
|
||||||
)}
|
|
||||||
{hasErrorOtherThanThreshold && (
|
|
||||||
<Result type="error" small className="mt-2">
|
|
||||||
Something went wrong while deleting the URL :(
|
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { formatIsoDate } from '../../utils/helpers/date';
|
||||||
import { ShortUrl, ShortUrlMeta, ShortUrlModalProps } from '../data';
|
import { ShortUrl, ShortUrlMeta, ShortUrlModalProps } from '../data';
|
||||||
import { handleEventPreventingDefault, Nullable, OptionalString } from '../../utils/utils';
|
import { handleEventPreventingDefault, Nullable, OptionalString } from '../../utils/utils';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
interface EditMetaModalConnectProps extends ShortUrlModalProps {
|
interface EditMetaModalConnectProps extends ShortUrlModalProps {
|
||||||
shortUrlMeta: ShortUrlMetaEdition;
|
shortUrlMeta: ShortUrlMetaEdition;
|
||||||
|
@ -27,7 +28,7 @@ const dateOrNull = (shortUrl: ShortUrl | undefined, dateName: 'validSince' | 'va
|
||||||
const EditMetaModal = (
|
const EditMetaModal = (
|
||||||
{ isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta }: EditMetaModalConnectProps,
|
{ isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta }: EditMetaModalConnectProps,
|
||||||
) => {
|
) => {
|
||||||
const { saving, error } = shortUrlMeta;
|
const { saving, error, errorData } = shortUrlMeta;
|
||||||
const url = shortUrl && (shortUrl.shortUrl || '');
|
const url = shortUrl && (shortUrl.shortUrl || '');
|
||||||
const [ validSince, setValidSince ] = useState(dateOrNull(shortUrl, 'validSince'));
|
const [ validSince, setValidSince ] = useState(dateOrNull(shortUrl, 'validSince'));
|
||||||
const [ validUntil, setValidUntil ] = useState(dateOrNull(shortUrl, 'validUntil'));
|
const [ validUntil, setValidUntil ] = useState(dateOrNull(shortUrl, 'validUntil'));
|
||||||
|
@ -78,9 +79,13 @@ const EditMetaModal = (
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setMaxVisits(Number(e.target.value))}
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setMaxVisits(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
Something went wrong while saving the metadata :(
|
<ShlinkApiError
|
||||||
|
errorData={errorData}
|
||||||
|
fallbackMessage="Something went wrong while saving the metadata :("
|
||||||
|
/>
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ShortUrlEdition } from '../reducers/shortUrlEdition';
|
||||||
import { handleEventPreventingDefault, hasValue, OptionalString } from '../../utils/utils';
|
import { handleEventPreventingDefault, hasValue, OptionalString } from '../../utils/utils';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
interface EditShortUrlModalProps extends ShortUrlModalProps {
|
interface EditShortUrlModalProps extends ShortUrlModalProps {
|
||||||
shortUrlEdition: ShortUrlEdition;
|
shortUrlEdition: ShortUrlEdition;
|
||||||
|
@ -12,7 +13,7 @@ interface EditShortUrlModalProps extends ShortUrlModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShortUrl }: EditShortUrlModalProps) => {
|
const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShortUrl }: EditShortUrlModalProps) => {
|
||||||
const { saving, error } = shortUrlEdition;
|
const { saving, error, errorData } = shortUrlEdition;
|
||||||
const url = shortUrl?.shortUrl ?? '';
|
const url = shortUrl?.shortUrl ?? '';
|
||||||
const [ longUrl, setLongUrl ] = useState(shortUrl.longUrl);
|
const [ longUrl, setLongUrl ] = useState(shortUrl.longUrl);
|
||||||
|
|
||||||
|
@ -36,7 +37,10 @@ const EditShortUrlModal = ({ isOpen, toggle, shortUrl, shortUrlEdition, editShor
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
Something went wrong while saving the long URL :(
|
<ShlinkApiError
|
||||||
|
errorData={errorData}
|
||||||
|
fallbackMessage="Something went wrong while saving the long URL :("
|
||||||
|
/>
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ShortUrlModalProps } from '../data';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { TagsSelectorProps } from '../../tags/helpers/TagsSelector';
|
import { TagsSelectorProps } from '../../tags/helpers/TagsSelector';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
interface EditTagsModalProps extends ShortUrlModalProps {
|
interface EditTagsModalProps extends ShortUrlModalProps {
|
||||||
shortUrlTags: ShortUrlTags;
|
shortUrlTags: ShortUrlTags;
|
||||||
|
@ -20,6 +21,7 @@ const EditTagsModal = (TagsSelector: FC<TagsSelectorProps>) => (
|
||||||
|
|
||||||
useEffect(() => resetShortUrlsTags, []);
|
useEffect(() => resetShortUrlsTags, []);
|
||||||
|
|
||||||
|
const { saving, error, errorData } = shortUrlTags;
|
||||||
const url = shortUrl?.shortUrl ?? '';
|
const url = shortUrl?.shortUrl ?? '';
|
||||||
const saveTags = async () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags)
|
const saveTags = async () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags)
|
||||||
.then(toggle)
|
.then(toggle)
|
||||||
|
@ -32,16 +34,16 @@ const EditTagsModal = (TagsSelector: FC<TagsSelectorProps>) => (
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<TagsSelector tags={selectedTags} onChange={setSelectedTags} />
|
<TagsSelector tags={selectedTags} onChange={setSelectedTags} />
|
||||||
{shortUrlTags.error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
Something went wrong while saving the tags :(
|
<ShlinkApiError errorData={errorData} fallbackMessage="Something went wrong while saving the tags :(" />
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||||
<button className="btn btn-primary" type="button" disabled={shortUrlTags.saving} onClick={saveTags}>
|
<button className="btn btn-primary" type="button" disabled={saving} onClick={saveTags}>
|
||||||
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
|
{saving ? 'Saving tags...' : 'Save tags'}
|
||||||
</button>
|
</button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { GetState } from '../../container/types';
|
||||||
import { ShortUrl, ShortUrlData } from '../data';
|
import { ShortUrl, ShortUrlData } from '../data';
|
||||||
import { buildReducer, buildActionCreator } from '../../utils/helpers/redux';
|
import { buildReducer, buildActionCreator } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
||||||
|
@ -15,21 +17,26 @@ export interface ShortUrlCreation {
|
||||||
result: ShortUrl | null;
|
result: ShortUrl | null;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateShortUrlAction extends Action<string> {
|
export interface CreateShortUrlAction extends Action<string> {
|
||||||
result: ShortUrl;
|
result: ShortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateShortUrlFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlCreation = {
|
const initialState: ShortUrlCreation = {
|
||||||
result: null,
|
result: null,
|
||||||
saving: false,
|
saving: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlCreation, CreateShortUrlAction>({
|
export default buildReducer<ShortUrlCreation, CreateShortUrlAction & CreateShortUrlFailedAction>({
|
||||||
[CREATE_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
[CREATE_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||||
[CREATE_SHORT_URL_ERROR]: (state) => ({ ...state, saving: false, error: true }),
|
[CREATE_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||||
[CREATE_SHORT_URL]: (_, { result }) => ({ result, saving: false, error: false }),
|
[CREATE_SHORT_URL]: (_, { result }) => ({ result, saving: false, error: false }),
|
||||||
[RESET_CREATE_SHORT_URL]: () => initialState,
|
[RESET_CREATE_SHORT_URL]: () => initialState,
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
@ -46,7 +53,7 @@ export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||||
|
|
||||||
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
|
dispatch<CreateShortUrlAction>({ type: CREATE_SHORT_URL, result });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: CREATE_SHORT_URL_ERROR });
|
dispatch<CreateShortUrlFailedAction>({ type: CREATE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ProblemDetailsError } from '../../utils/services/types';
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
export const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
||||||
|
@ -24,7 +25,7 @@ export interface DeleteShortUrlAction extends Action<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteShortUrlErrorAction extends Action<string> {
|
interface DeleteShortUrlErrorAction extends Action<string> {
|
||||||
errorData: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlDeletion = {
|
const initialState: ShortUrlDeletion = {
|
||||||
|
@ -51,7 +52,7 @@ export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||||
await deleteShortUrl(shortCode, domain);
|
await deleteShortUrl(shortCode, domain);
|
||||||
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
|
dispatch<DeleteShortUrlAction>({ type: SHORT_URL_DELETED, shortCode, domain });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch<DeleteShortUrlErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: e.response.data });
|
dispatch<DeleteShortUrlErrorAction>({ type: DELETE_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { GetState } from '../../container/types';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { ShortUrlIdentifier } from '../data';
|
import { ShortUrlIdentifier } from '../data';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
|
export const EDIT_SHORT_URL_START = 'shlink/shortUrlEdition/EDIT_SHORT_URL_START';
|
||||||
|
@ -16,12 +18,17 @@ export interface ShortUrlEdition {
|
||||||
longUrl: string | null;
|
longUrl: string | null;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShortUrlEditedAction extends Action<string>, ShortUrlIdentifier {
|
export interface ShortUrlEditedAction extends Action<string>, ShortUrlIdentifier {
|
||||||
longUrl: string;
|
longUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlEditionFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlEdition = {
|
const initialState: ShortUrlEdition = {
|
||||||
shortCode: null,
|
shortCode: null,
|
||||||
longUrl: null,
|
longUrl: null,
|
||||||
|
@ -29,9 +36,9 @@ const initialState: ShortUrlEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlEdition, ShortUrlEditedAction>({
|
export default buildReducer<ShortUrlEdition, ShortUrlEditedAction & ShortUrlEditionFailedAction>({
|
||||||
[EDIT_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
[EDIT_SHORT_URL_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||||
[EDIT_SHORT_URL_ERROR]: (state) => ({ ...state, saving: false, error: true }),
|
[EDIT_SHORT_URL_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||||
[SHORT_URL_EDITED]: (_, { shortCode, longUrl }) => ({ shortCode, longUrl, saving: false, error: false }),
|
[SHORT_URL_EDITED]: (_, { shortCode, longUrl }) => ({ shortCode, longUrl, saving: false, error: false }),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
|
@ -47,7 +54,7 @@ export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
await updateShortUrlMeta(shortCode, domain, { longUrl });
|
await updateShortUrlMeta(shortCode, domain, { longUrl });
|
||||||
dispatch<ShortUrlEditedAction>({ shortCode, longUrl, domain, type: SHORT_URL_EDITED });
|
dispatch<ShortUrlEditedAction>({ shortCode, longUrl, domain, type: SHORT_URL_EDITED });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_SHORT_URL_ERROR });
|
dispatch<ShortUrlEditionFailedAction>({ type: EDIT_SHORT_URL_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { GetState } from '../../container/types';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START';
|
export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START';
|
||||||
|
@ -17,12 +19,17 @@ export interface ShortUrlMetaEdition {
|
||||||
meta: ShortUrlMeta;
|
meta: ShortUrlMeta;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShortUrlMetaEditedAction extends Action<string>, ShortUrlIdentifier {
|
export interface ShortUrlMetaEditedAction extends Action<string>, ShortUrlIdentifier {
|
||||||
meta: ShortUrlMeta;
|
meta: ShortUrlMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlMetaEditionFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlMetaEdition = {
|
const initialState: ShortUrlMetaEdition = {
|
||||||
shortCode: null,
|
shortCode: null,
|
||||||
meta: {},
|
meta: {},
|
||||||
|
@ -30,9 +37,9 @@ const initialState: ShortUrlMetaEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlMetaEdition, ShortUrlMetaEditedAction>({
|
export default buildReducer<ShortUrlMetaEdition, ShortUrlMetaEditedAction & ShortUrlMetaEditionFailedAction>({
|
||||||
[EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }),
|
[EDIT_SHORT_URL_META_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||||
[EDIT_SHORT_URL_META_ERROR]: (state) => ({ ...state, saving: false, error: true }),
|
[EDIT_SHORT_URL_META_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||||
[SHORT_URL_META_EDITED]: (_, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }),
|
[SHORT_URL_META_EDITED]: (_, { shortCode, meta }) => ({ shortCode, meta, saving: false, error: false }),
|
||||||
[RESET_EDIT_SHORT_URL_META]: () => initialState,
|
[RESET_EDIT_SHORT_URL_META]: () => initialState,
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
@ -49,7 +56,7 @@ export const editShortUrlMeta = (buildShlinkApiClient: ShlinkApiClientBuilder) =
|
||||||
await updateShortUrlMeta(shortCode, domain, meta);
|
await updateShortUrlMeta(shortCode, domain, meta);
|
||||||
dispatch<ShortUrlMetaEditedAction>({ shortCode, meta, domain, type: SHORT_URL_META_EDITED });
|
dispatch<ShortUrlMetaEditedAction>({ shortCode, meta, domain, type: SHORT_URL_META_EDITED });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_SHORT_URL_META_ERROR });
|
dispatch<ShortUrlMetaEditionFailedAction>({ type: EDIT_SHORT_URL_META_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { GetState } from '../../container/types';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { ShortUrlIdentifier } from '../data';
|
import { ShortUrlIdentifier } from '../data';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
|
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
|
||||||
|
@ -17,12 +19,17 @@ export interface ShortUrlTags {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditShortUrlTagsAction extends Action<string>, ShortUrlIdentifier {
|
export interface EditShortUrlTagsAction extends Action<string>, ShortUrlIdentifier {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditShortUrlTagsFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ShortUrlTags = {
|
const initialState: ShortUrlTags = {
|
||||||
shortCode: null,
|
shortCode: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -30,9 +37,9 @@ const initialState: ShortUrlTags = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlTags, EditShortUrlTagsAction>({
|
export default buildReducer<ShortUrlTags, EditShortUrlTagsAction & EditShortUrlTagsFailedAction>({
|
||||||
[EDIT_SHORT_URL_TAGS_START]: (state) => ({ ...state, saving: true, error: false }),
|
[EDIT_SHORT_URL_TAGS_START]: (state) => ({ ...state, saving: true, error: false }),
|
||||||
[EDIT_SHORT_URL_TAGS_ERROR]: (state) => ({ ...state, saving: false, error: true }),
|
[EDIT_SHORT_URL_TAGS_ERROR]: (state, { errorData }) => ({ ...state, saving: false, error: true, errorData }),
|
||||||
[SHORT_URL_TAGS_EDITED]: (_, { shortCode, tags }) => ({ shortCode, tags, saving: false, error: false }),
|
[SHORT_URL_TAGS_EDITED]: (_, { shortCode, tags }) => ({ shortCode, tags, saving: false, error: false }),
|
||||||
[RESET_EDIT_SHORT_URL_TAGS]: () => initialState,
|
[RESET_EDIT_SHORT_URL_TAGS]: () => initialState,
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
@ -50,7 +57,7 @@ export const editShortUrlTags = (buildShlinkApiClient: ShlinkApiClientBuilder) =
|
||||||
|
|
||||||
dispatch<EditShortUrlTagsAction>({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED });
|
dispatch<EditShortUrlTagsAction>({ tags: normalizedTags, shortCode, domain, type: SHORT_URL_TAGS_EDITED });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR });
|
dispatch<EditShortUrlTagsFailedAction>({ type: EDIT_SHORT_URL_TAGS_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import SearchField from '../utils/SearchField';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { TagsList as TagsListState } from './reducers/tagsList';
|
import { TagsList as TagsListState } from './reducers/tagsList';
|
||||||
import { TagCardProps } from './TagCard';
|
import { TagCardProps } from './TagCard';
|
||||||
|
|
||||||
|
@ -33,7 +34,11 @@ const TagsList = (TagCard: FC<TagCardProps>) => boundToMercureHub((
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagsList.error) {
|
if (tagsList.error) {
|
||||||
return <Result type="error">Error loading tags :(</Result>;
|
return (
|
||||||
|
<Result type="error">
|
||||||
|
<ShlinkApiError errorData={tagsList.errorData} fallbackMessage="Error loading tags :(" />
|
||||||
|
</Result>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsCount = tagsList.filteredTags.length;
|
const tagsCount = tagsList.filteredTags.length;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { TagDeletion } from '../reducers/tagDelete';
|
import { TagDeletion } from '../reducers/tagDelete';
|
||||||
import { TagModalProps } from '../data';
|
import { TagModalProps } from '../data';
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
|
||||||
interface DeleteTagConfirmModalProps extends TagModalProps {
|
interface DeleteTagConfirmModalProps extends TagModalProps {
|
||||||
deleteTag: (tag: string) => Promise<void>;
|
deleteTag: (tag: string) => Promise<void>;
|
||||||
|
@ -12,6 +13,7 @@ interface DeleteTagConfirmModalProps extends TagModalProps {
|
||||||
const DeleteTagConfirmModal = (
|
const DeleteTagConfirmModal = (
|
||||||
{ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps,
|
{ tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps,
|
||||||
) => {
|
) => {
|
||||||
|
const { deleting, error, errorData } = tagDelete;
|
||||||
const doDelete = async () => {
|
const doDelete = async () => {
|
||||||
await deleteTag(tag);
|
await deleteTag(tag);
|
||||||
tagDeleted(tag);
|
tagDeleted(tag);
|
||||||
|
@ -25,16 +27,16 @@ const DeleteTagConfirmModal = (
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
Are you sure you want to delete tag <b>{tag}</b>?
|
Are you sure you want to delete tag <b>{tag}</b>?
|
||||||
{tagDelete.error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
Something went wrong while deleting the tag :(
|
<ShlinkApiError errorData={errorData} fallbackMessage="Something went wrong while deleting the tag :(" />
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||||
<button className="btn btn-danger" disabled={tagDelete.deleting} onClick={doDelete}>
|
<button className="btn btn-danger" disabled={deleting} onClick={doDelete}>
|
||||||
{tagDelete.deleting ? 'Deleting tag...' : 'Delete tag'}
|
{deleting ? 'Deleting tag...' : 'Delete tag'}
|
||||||
</button>
|
</button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -8,8 +8,9 @@ import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||||
import { TagModalProps } from '../data';
|
import { TagModalProps } from '../data';
|
||||||
import { TagEdition } from '../reducers/tagEdit';
|
import { TagEdition } from '../reducers/tagEdit';
|
||||||
import './EditTagModal.scss';
|
|
||||||
import { Result } from '../../utils/Result';
|
import { Result } from '../../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../../api/ShlinkApiError';
|
||||||
|
import './EditTagModal.scss';
|
||||||
|
|
||||||
interface EditTagModalProps extends TagModalProps {
|
interface EditTagModalProps extends TagModalProps {
|
||||||
tagEdit: TagEdition;
|
tagEdit: TagEdition;
|
||||||
|
@ -23,6 +24,7 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
||||||
const [ newTagName, setNewTagName ] = useState(tag);
|
const [ newTagName, setNewTagName ] = useState(tag);
|
||||||
const [ color, setColor ] = useState(getColorForKey(tag));
|
const [ color, setColor ] = useState(getColorForKey(tag));
|
||||||
const [ showColorPicker, toggleColorPicker, , hideColorPicker ] = useToggle();
|
const [ showColorPicker, toggleColorPicker, , hideColorPicker ] = useToggle();
|
||||||
|
const { editing, error, errorData } = tagEdit;
|
||||||
const saveTag = handleEventPreventingDefault(async () => editTag(tag, newTagName, color)
|
const saveTag = handleEventPreventingDefault(async () => editTag(tag, newTagName, color)
|
||||||
.then(() => tagEdited(tag, newTagName, color))
|
.then(() => tagEdited(tag, newTagName, color))
|
||||||
.then(toggle)
|
.then(toggle)
|
||||||
|
@ -55,17 +57,15 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tagEdit.error && (
|
{error && (
|
||||||
<Result type="error" small className="mt-2">
|
<Result type="error" small className="mt-2">
|
||||||
Something went wrong while editing the tag :(
|
<ShlinkApiError errorData={errorData} fallbackMessage="Something went wrong while editing the tag :(" />
|
||||||
</Result>
|
</Result>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
|
<button type="button" className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||||
<button type="submit" className="btn btn-primary" disabled={tagEdit.editing}>
|
<button type="submit" className="btn btn-primary" disabled={editing}>{editing ? 'Saving...' : 'Save'}</button>
|
||||||
{tagEdit.editing ? 'Saving...' : 'Save'}
|
|
||||||
</button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Action, Dispatch } from 'redux';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
||||||
|
@ -13,20 +15,25 @@ export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
||||||
export interface TagDeletion {
|
export interface TagDeletion {
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteTagAction extends Action<string> {
|
export interface DeleteTagAction extends Action<string> {
|
||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteTagFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: TagDeletion = {
|
const initialState: TagDeletion = {
|
||||||
deleting: false,
|
deleting: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer({
|
export default buildReducer<TagDeletion, DeleteTagFailedAction>({
|
||||||
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
||||||
[DELETE_TAG_ERROR]: () => ({ deleting: false, error: true }),
|
[DELETE_TAG_ERROR]: (_, { errorData }) => ({ deleting: false, error: true, errorData }),
|
||||||
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
|
@ -41,7 +48,7 @@ export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag:
|
||||||
await deleteTags([ tag ]);
|
await deleteTags([ tag ]);
|
||||||
dispatch({ type: DELETE_TAG });
|
dispatch({ type: DELETE_TAG });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: DELETE_TAG_ERROR });
|
dispatch<DeleteTagFailedAction>({ type: DELETE_TAG_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||||
|
@ -18,6 +20,7 @@ export interface TagEdition {
|
||||||
newName: string;
|
newName: string;
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditTagAction extends Action<string> {
|
export interface EditTagAction extends Action<string> {
|
||||||
|
@ -26,6 +29,10 @@ export interface EditTagAction extends Action<string> {
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditTagFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: TagEdition = {
|
const initialState: TagEdition = {
|
||||||
oldName: '',
|
oldName: '',
|
||||||
newName: '',
|
newName: '',
|
||||||
|
@ -33,9 +40,9 @@ const initialState: TagEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<TagEdition, EditTagAction>({
|
export default buildReducer<TagEdition, EditTagAction & EditTagFailedAction>({
|
||||||
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
||||||
[EDIT_TAG_ERROR]: (state) => ({ ...state, editing: false, error: true }),
|
[EDIT_TAG_ERROR]: (state, { errorData }) => ({ ...state, editing: false, error: true, errorData }),
|
||||||
[EDIT_TAG]: (_, action) => ({
|
[EDIT_TAG]: (_, action) => ({
|
||||||
...pick([ 'oldName', 'newName' ], action),
|
...pick([ 'oldName', 'newName' ], action),
|
||||||
editing: false,
|
editing: false,
|
||||||
|
@ -56,7 +63,7 @@ export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGener
|
||||||
colorGenerator.setColorForKey(newName, color);
|
colorGenerator.setColorForKey(newName, color);
|
||||||
dispatch({ type: EDIT_TAG, oldName, newName });
|
dispatch({ type: EDIT_TAG, oldName, newName });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_TAG_ERROR });
|
dispatch<EditTagFailedAction>({ type: EDIT_TAG_ERROR, errorData: parseApiError(e) });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { isEmpty, reject } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
import { CREATE_VISITS, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkTags } from '../../utils/services/types';
|
import { ProblemDetailsError, ShlinkTags } from '../../utils/services/types';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
import { TagStats } from '../data';
|
import { TagStats } from '../data';
|
||||||
import { CreateVisit, Stats } from '../../visits/types';
|
import { CreateVisit, Stats } from '../../visits/types';
|
||||||
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||||
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
||||||
|
@ -25,6 +26,7 @@ export interface TagsList {
|
||||||
stats: TagsStatsMap;
|
stats: TagsStatsMap;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListTagsAction extends Action<string> {
|
interface ListTagsAction extends Action<string> {
|
||||||
|
@ -32,11 +34,21 @@ interface ListTagsAction extends Action<string> {
|
||||||
stats: TagsStatsMap;
|
stats: TagsStatsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ListTagsFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
interface FilterTagsAction extends Action<string> {
|
interface FilterTagsAction extends Action<string> {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListTagsCombinedAction = ListTagsAction & DeleteTagAction & CreateVisitsAction & EditTagAction & FilterTagsAction;
|
type ListTagsCombinedAction = ListTagsAction
|
||||||
|
& DeleteTagAction
|
||||||
|
& CreateVisitsAction
|
||||||
|
& EditTagAction
|
||||||
|
& FilterTagsAction
|
||||||
|
& ListTagsFailedAction;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -74,7 +86,7 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
|
||||||
|
|
||||||
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
||||||
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
||||||
[LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }),
|
[LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||||
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
||||||
[TAG_DELETED]: (state, { tag }) => ({
|
[TAG_DELETED]: (state, { tag }) => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -119,7 +131,7 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
|
||||||
|
|
||||||
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
|
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: LIST_TAGS_ERROR });
|
dispatch<ListTagsFailedAction>({ type: LIST_TAGS_ERROR, errorData: parseApiError(e) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ import {
|
||||||
ShlinkVisitsOverview,
|
ShlinkVisitsOverview,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
// TODO Move this file to api module
|
||||||
|
|
||||||
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||||
const rejectNilProps = reject(isNil);
|
const rejectNilProps = reject(isNil);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data'
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import ShlinkApiClient from './ShlinkApiClient';
|
import ShlinkApiClient from './ShlinkApiClient';
|
||||||
|
|
||||||
|
// TODO Move this file to api module
|
||||||
|
|
||||||
const apiClients: Record<string, ShlinkApiClient> = {};
|
const apiClients: Record<string, ShlinkApiClient> = {};
|
||||||
|
|
||||||
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
|
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Visit } from '../../visits/types'; // FIXME Should be defined as part o
|
||||||
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data'; // FIXME Should be defined as part of this module
|
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data'; // FIXME Should be defined as part of this module
|
||||||
import { OptionalString } from '../utils';
|
import { OptionalString } from '../utils';
|
||||||
|
|
||||||
|
// TODO Move this file to api module
|
||||||
|
|
||||||
export interface ShlinkShortUrlsResponse {
|
export interface ShlinkShortUrlsResponse {
|
||||||
data: ShortUrl[];
|
data: ShortUrl[];
|
||||||
pagination: ShlinkPaginator;
|
pagination: ShlinkPaginator;
|
||||||
|
@ -25,12 +27,12 @@ interface ShlinkTagsStats {
|
||||||
|
|
||||||
export interface ShlinkTags {
|
export interface ShlinkTags {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
stats?: ShlinkTagsStats[]; // Is only optional in old Shlink versions
|
stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkTagsResponse {
|
export interface ShlinkTagsResponse {
|
||||||
data: string[];
|
data: string[];
|
||||||
stats?: ShlinkTagsStats[]; // Is only optional in old Shlink versions
|
stats?: ShlinkTagsStats[]; // Is only optional in Shlink older than v2.2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkPaginator {
|
export interface ShlinkPaginator {
|
||||||
|
@ -41,7 +43,7 @@ export interface ShlinkPaginator {
|
||||||
|
|
||||||
export interface ShlinkVisits {
|
export interface ShlinkVisits {
|
||||||
data: Visit[];
|
data: Visit[];
|
||||||
pagination?: ShlinkPaginator; // Is only optional in old Shlink versions
|
pagination: ShlinkPaginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkVisitsOverview {
|
export interface ShlinkVisitsOverview {
|
||||||
|
@ -60,14 +62,6 @@ export interface ShlinkShortUrlMeta extends ShortUrlMeta {
|
||||||
longUrl?: string;
|
longUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProblemDetailsError {
|
|
||||||
type: string;
|
|
||||||
detail: string;
|
|
||||||
title: string;
|
|
||||||
status: number;
|
|
||||||
[extraProps: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShlinkDomain {
|
export interface ShlinkDomain {
|
||||||
domain: string;
|
domain: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
@ -76,3 +70,27 @@ export interface ShlinkDomain {
|
||||||
export interface ShlinkDomainsResponse {
|
export interface ShlinkDomainsResponse {
|
||||||
data: ShlinkDomain[];
|
data: ShlinkDomain[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProblemDetailsError {
|
||||||
|
type: string;
|
||||||
|
detail: string;
|
||||||
|
title: string;
|
||||||
|
status: number;
|
||||||
|
[extraProps: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvalidArgumentError extends ProblemDetailsError {
|
||||||
|
type: 'INVALID_ARGUMENT';
|
||||||
|
invalidElements: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvalidShortUrlDeletion extends ProblemDetailsError {
|
||||||
|
type: 'INVALID_SHORTCODE_DELETION';
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
|
||||||
|
error?.type === 'INVALID_ARGUMENT';
|
||||||
|
|
||||||
|
export const isInvalidDeletionError = (error?: ProblemDetailsError): error is InvalidShortUrlDeletion =>
|
||||||
|
error?.type === 'INVALID_SHORTCODE_DELETION';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { ShlinkVisitsParams } from '../utils/services/types';
|
import { ShlinkVisitsParams } from '../utils/services/types';
|
||||||
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
import LineChartCard from './helpers/LineChartCard';
|
import LineChartCard from './helpers/LineChartCard';
|
||||||
|
@ -83,7 +84,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
|
|
||||||
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
|
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
|
||||||
};
|
};
|
||||||
const { visits, loading, loadingLarge, error, progress } = visitsInfo;
|
const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo;
|
||||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
||||||
() => processStatsFromVisits(normalizedVisits),
|
() => processStatsFromVisits(normalizedVisits),
|
||||||
|
@ -131,7 +132,11 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Result type="error">An error occurred while loading visits :(</Result>;
|
return (
|
||||||
|
<Result type="error">
|
||||||
|
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while loading visits :(" />
|
||||||
|
</Result>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty(visits)) {
|
if (isEmpty(visits)) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { flatten, prop, range, splitEvery } from 'ramda';
|
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { ShlinkPaginator, ShlinkVisits } from '../../utils/services/types';
|
import { ShlinkPaginator, ShlinkVisits } from '../../utils/services/types';
|
||||||
import { Visit } from '../types';
|
import { Visit, VisitsLoadFailedAction } from '../types';
|
||||||
|
import { parseApiError } from '../../api/util';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 5000;
|
const ITEMS_PER_PAGE = 5000;
|
||||||
const PARALLEL_REQUESTS_COUNT = 4;
|
const PARALLEL_REQUESTS_COUNT = 4;
|
||||||
|
@ -71,6 +72,6 @@ export const getVisitsWithLoader = async <T extends Action<string> & { visits: V
|
||||||
|
|
||||||
dispatch({ ...extraFinishActionData, visits, type: actionMap.finish });
|
dispatch({ ...extraFinishActionData, visits, type: actionMap.finish });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: actionMap.error });
|
dispatch<VisitsLoadFailedAction>({ type: actionMap.error, errorData: parseApiError(e) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { shortUrlMatches } from '../../short-urls/helpers';
|
import { shortUrlMatches } from '../../short-urls/helpers';
|
||||||
import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types';
|
||||||
import { ShortUrlIdentifier } from '../../short-urls/data';
|
import { ShortUrlIdentifier } from '../../short-urls/data';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
@ -24,7 +24,10 @@ interface ShortUrlVisitsAction extends Action<string>, ShortUrlIdentifier {
|
||||||
visits: Visit[];
|
visits: Visit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction & VisitsLoadProgressChangedAction & CreateVisitsAction;
|
type ShortUrlVisitsCombinedAction = ShortUrlVisitsAction
|
||||||
|
& VisitsLoadProgressChangedAction
|
||||||
|
& CreateVisitsAction
|
||||||
|
& VisitsLoadFailedAction;
|
||||||
|
|
||||||
const initialState: ShortUrlVisits = {
|
const initialState: ShortUrlVisits = {
|
||||||
visits: [],
|
visits: [],
|
||||||
|
@ -39,7 +42,7 @@ const initialState: ShortUrlVisits = {
|
||||||
|
|
||||||
export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
|
export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
|
||||||
[GET_SHORT_URL_VISITS_START]: () => ({ ...initialState, loading: true }),
|
[GET_SHORT_URL_VISITS_START]: () => ({ ...initialState, loading: true }),
|
||||||
[GET_SHORT_URL_VISITS_ERROR]: () => ({ ...initialState, error: true }),
|
[GET_SHORT_URL_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||||
[GET_SHORT_URL_VISITS]: (_, { visits, shortCode, domain }) => ({
|
[GET_SHORT_URL_VISITS]: (_, { visits, shortCode, domain }) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
visits,
|
visits,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { Visit, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
|
@ -24,6 +24,11 @@ export interface TagVisitsAction extends Action<string> {
|
||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TagsVisitsCombinedAction = TagVisitsAction
|
||||||
|
& VisitsLoadProgressChangedAction
|
||||||
|
& CreateVisitsAction
|
||||||
|
& VisitsLoadFailedAction;
|
||||||
|
|
||||||
const initialState: TagVisits = {
|
const initialState: TagVisits = {
|
||||||
visits: [],
|
visits: [],
|
||||||
tag: '',
|
tag: '',
|
||||||
|
@ -34,9 +39,9 @@ const initialState: TagVisits = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<TagVisits, TagVisitsAction & VisitsLoadProgressChangedAction & CreateVisitsAction>({
|
export default buildReducer<TagVisits, TagsVisitsCombinedAction>({
|
||||||
[GET_TAG_VISITS_START]: () => ({ ...initialState, loading: true }),
|
[GET_TAG_VISITS_START]: () => ({ ...initialState, loading: true }),
|
||||||
[GET_TAG_VISITS_ERROR]: () => ({ ...initialState, error: true }),
|
[GET_TAG_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||||
[GET_TAG_VISITS]: (_, { visits, tag }) => ({ ...initialState, visits, tag }),
|
[GET_TAG_VISITS]: (_, { visits, tag }) => ({ ...initialState, visits, tag }),
|
||||||
[GET_TAG_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
[GET_TAG_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
||||||
[GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
[GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { ShortUrl } from '../../short-urls/data';
|
import { ShortUrl } from '../../short-urls/data';
|
||||||
|
import { ProblemDetailsError } from '../../utils/services/types';
|
||||||
|
|
||||||
export interface VisitsInfo {
|
export interface VisitsInfo {
|
||||||
visits: Visit[];
|
visits: Visit[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadingLarge: boolean;
|
loadingLarge: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
progress: number;
|
progress: number;
|
||||||
cancelLoad: boolean;
|
cancelLoad: boolean;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +16,10 @@ export interface VisitsLoadProgressChangedAction extends Action<string> {
|
||||||
progress: number;
|
progress: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VisitsLoadFailedAction extends Action<string> {
|
||||||
|
errorData?: ProblemDetailsError;
|
||||||
|
}
|
||||||
|
|
||||||
interface VisitLocation {
|
interface VisitLocation {
|
||||||
countryCode: string | null;
|
countryCode: string | null;
|
||||||
countryName: string | null;
|
countryName: string | null;
|
||||||
|
|
|
@ -33,28 +33,6 @@ describe('<DeleteShortUrlModal />', () => {
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
it.each([
|
|
||||||
[
|
|
||||||
{ type: 'INVALID_SHORTCODE_DELETION' },
|
|
||||||
'This short URL has received too many visits, and therefore, it cannot be deleted.',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ type: 'INVALID_SHORTCODE_DELETION', threshold: 8 },
|
|
||||||
'This short URL has received more than 8 visits, and therefore, it cannot be deleted.',
|
|
||||||
],
|
|
||||||
])('shows threshold error message when threshold error occurs', (errorData: Partial<ProblemDetailsError>, expectedMessage) => {
|
|
||||||
const wrapper = createWrapper({
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
shortCode: 'abc123',
|
|
||||||
errorData: Mock.of<ProblemDetailsError>(errorData),
|
|
||||||
});
|
|
||||||
const warning = wrapper.find(Result).filterWhere((result) => result.prop('type') === 'warning');
|
|
||||||
|
|
||||||
expect(warning).toHaveLength(1);
|
|
||||||
expect(warning.html()).toContain(expectedMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows generic error when non-threshold error occurs', () => {
|
it('shows generic error when non-threshold error occurs', () => {
|
||||||
const wrapper = createWrapper({
|
const wrapper = createWrapper({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -103,7 +103,7 @@ describe('shortUrlVisitsReducer', () => {
|
||||||
beforeEach(() => dispatchMock.mockReset());
|
beforeEach(() => dispatchMock.mockReset());
|
||||||
|
|
||||||
it('dispatches start and error when promise is rejected', async () => {
|
it('dispatches start and error when promise is rejected', async () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.reject() as any);
|
const ShlinkApiClient = buildApiClientMock(Promise.reject({}));
|
||||||
|
|
||||||
await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ describe('tagVisitsReducer', () => {
|
||||||
beforeEach(jest.resetAllMocks);
|
beforeEach(jest.resetAllMocks);
|
||||||
|
|
||||||
it('dispatches start and error when promise is rejected', async () => {
|
it('dispatches start and error when promise is rejected', async () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
const ShlinkApiClient = buildApiClientMock(Promise.reject({}));
|
||||||
|
|
||||||
await getTagVisits(() => ShlinkApiClient)('foo')(dispatchMock, getState);
|
await getTagVisits(() => ShlinkApiClient)('foo')(dispatchMock, getState);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue