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 { StateFlagTimeout } from '../../utils/helpers/hooks';
import { Result } from '../../utils/Result'; import { Result } from '../../utils/Result';
import './CreateShortUrlResult.scss'; import './CreateShortUrlResult.scss';
import { isInvalidArgumentError } from '../../utils/services/types'; import { ShlinkApiError } from '../../api/ShlinkApiError';
export interface CreateShortUrlResultProps extends ShortUrlCreation { export interface CreateShortUrlResultProps extends ShortUrlCreation {
resetCreateShortUrl: () => void; resetCreateShortUrl: () => void;
@ -29,8 +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} />}
{errorData?.detail ?? 'An error occurred while creating the URL :('} <ShlinkApiError errorData={errorData} fallbackMessage="An error occurred while creating the URL :(" />
{isInvalidArgumentError(errorData) && <p>Invalid elements: [{errorData.invalidElements.join(', ')}]</p>}
</Result> </Result>
); );
} }

View file

@ -6,6 +6,7 @@ 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'; import { isInvalidDeletionError } from '../../utils/services/types';
import { ShlinkApiError } from '../../api/ShlinkApiError';
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps { interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
shortUrlDeletion: ShortUrlDeletion; shortUrlDeletion: ShortUrlDeletion;
@ -49,15 +50,9 @@ const DeleteShortUrlModal = (
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
/> />
{error && isInvalidDeletionError(errorData) && ( {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>
)}
{error && !isInvalidDeletionError(errorData) && (
<Result type="error" small className="mt-2">
{errorData?.detail ?? 'Something went wrong while deleting the URL :('}
</Result> </Result>
)} )}
</ModalBody> </ModalBody>

View file

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

View file

@ -4,6 +4,7 @@ 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';
/* 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 +18,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 +36,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 +55,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: e.response?.data });
throw e; throw e;
} }

View file

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

View file

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

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

View file

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