Created component holding the logic to render Shlink API errors

This commit is contained in:
Alejandro Celaya 2020-12-21 21:19:02 +01:00
parent f69f791790
commit 51379eb2a0
9 changed files with 43 additions and 39 deletions

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

View file

@ -9,7 +9,7 @@ import { ShortUrlCreation } from '../reducers/shortUrlCreation';
import { StateFlagTimeout } from '../../utils/helpers/hooks';
import { Result } from '../../utils/Result';
import './CreateShortUrlResult.scss';
import { isInvalidArgumentError } from '../../utils/services/types';
import { ShlinkApiError } from '../../api/ShlinkApiError';
export interface CreateShortUrlResultProps extends ShortUrlCreation {
resetCreateShortUrl: () => void;
@ -29,8 +29,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (
return (
<Result type="error" className="mt-3">
{canBeClosed && <FontAwesomeIcon icon={closeIcon} className="float-right pointer" onClick={resetCreateShortUrl} />}
{errorData?.detail ?? 'An error occurred while creating the URL :('}
{isInvalidArgumentError(errorData) && <p>Invalid elements: [{errorData.invalidElements.join(', ')}]</p>}
<ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while creating the URL :(" />
</Result>
);
}

View file

@ -6,6 +6,7 @@ import { ShortUrlModalProps } from '../data';
import { handleEventPreventingDefault, OptionalString } from '../../utils/utils';
import { Result } from '../../utils/Result';
import { isInvalidDeletionError } from '../../utils/services/types';
import { ShlinkApiError } from '../../api/ShlinkApiError';
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
shortUrlDeletion: ShortUrlDeletion;
@ -49,15 +50,9 @@ const DeleteShortUrlModal = (
onChange={(e) => setInputValue(e.target.value)}
/>
{error && isInvalidDeletionError(errorData) && (
<Result type="warning" small className="mt-2">
{errorData.threshold && `This short URL has received more than ${errorData.threshold} visits, and therefore, it cannot be deleted.`}
{!errorData.threshold && 'This short URL has received too many visits, and therefore, it cannot be deleted.'}
</Result>
)}
{error && !isInvalidDeletionError(errorData) && (
<Result type="error" small className="mt-2">
{errorData?.detail ?? 'Something went wrong while deleting the URL :('}
{error && (
<Result type={isInvalidDeletionError(errorData) ? 'warning' : 'error'} small className="mt-2">
<ShlinkApiError errorData={errorData} fallbackMessage="Something went wrong while deleting the URL :(" />
</Result>
)}
</ModalBody>

View file

@ -11,6 +11,7 @@ import { formatIsoDate } from '../../utils/helpers/date';
import { ShortUrl, ShortUrlMeta, ShortUrlModalProps } from '../data';
import { handleEventPreventingDefault, Nullable, OptionalString } from '../../utils/utils';
import { Result } from '../../utils/Result';
import { ShlinkApiError } from '../../api/ShlinkApiError';
interface EditMetaModalConnectProps extends ShortUrlModalProps {
shortUrlMeta: ShortUrlMetaEdition;
@ -27,7 +28,7 @@ const dateOrNull = (shortUrl: ShortUrl | undefined, dateName: 'validSince' | 'va
const EditMetaModal = (
{ isOpen, toggle, shortUrl, shortUrlMeta, editShortUrlMeta, resetShortUrlMeta }: EditMetaModalConnectProps,
) => {
const { saving, error } = shortUrlMeta;
const { saving, error, errorData } = shortUrlMeta;
const url = shortUrl && (shortUrl.shortUrl || '');
const [ validSince, setValidSince ] = useState(dateOrNull(shortUrl, 'validSince'));
const [ validUntil, setValidUntil ] = useState(dateOrNull(shortUrl, 'validUntil'));
@ -78,9 +79,13 @@ const EditMetaModal = (
onChange={(e: ChangeEvent<HTMLInputElement>) => setMaxVisits(Number(e.target.value))}
/>
</FormGroup>
{error && (
<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>
)}
</ModalBody>

View file

@ -4,6 +4,7 @@ import { GetState } from '../../container/types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { OptionalString } from '../../utils/utils';
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
import { ProblemDetailsError } from '../../utils/services/types';
/* eslint-disable padding-line-between-statements */
export const EDIT_SHORT_URL_META_START = 'shlink/shortUrlMeta/EDIT_SHORT_URL_META_START';
@ -17,12 +18,17 @@ export interface ShortUrlMetaEdition {
meta: ShortUrlMeta;
saving: boolean;
error: boolean;
errorData?: ProblemDetailsError;
}
export interface ShortUrlMetaEditedAction extends Action<string>, ShortUrlIdentifier {
meta: ShortUrlMeta;
}
export interface ShortUrlMetaEditionFailedAction extends Action<string> {
errorData?: ProblemDetailsError;
}
const initialState: ShortUrlMetaEdition = {
shortCode: null,
meta: {},
@ -30,9 +36,9 @@ const initialState: ShortUrlMetaEdition = {
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_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 }),
[RESET_EDIT_SHORT_URL_META]: () => initialState,
}, initialState);
@ -49,7 +55,7 @@ export const editShortUrlMeta = (buildShlinkApiClient: ShlinkApiClientBuilder) =
await updateShortUrlMeta(shortCode, domain, meta);
dispatch<ShortUrlMetaEditedAction>({ shortCode, meta, domain, type: SHORT_URL_META_EDITED });
} catch (e) {
dispatch({ type: EDIT_SHORT_URL_META_ERROR });
dispatch<ShortUrlMetaEditionFailedAction>({ type: EDIT_SHORT_URL_META_ERROR, errorData: e.response?.data });
throw e;
}

View file

@ -18,6 +18,8 @@ import {
ShlinkVisitsOverview,
} from './types';
// TODO Move this file to api module
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
const rejectNilProps = reject(isNil);

View file

@ -4,6 +4,8 @@ import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data'
import { GetState } from '../../container/types';
import ShlinkApiClient from './ShlinkApiClient';
// TODO Move this file to api module
const apiClients: Record<string, ShlinkApiClient> = {};
const isGetState = (getStateOrSelectedServer: GetState | ServerWithId): getStateOrSelectedServer is GetState =>

View file

@ -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 { OptionalString } from '../utils';
// TODO Move this file to api module
export interface ShlinkShortUrlsResponse {
data: ShortUrl[];
pagination: ShlinkPaginator;

View file

@ -33,28 +33,6 @@ describe('<DeleteShortUrlModal />', () => {
afterEach(() => wrapper?.unmount());
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', () => {
const wrapper = createWrapper({
loading: false,