mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Created component holding the logic to render Shlink API errors
This commit is contained in:
parent
f69f791790
commit
51379eb2a0
9 changed files with 43 additions and 39 deletions
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>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue