mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Added generic error handling to AdminX (#18274)
refs https://github.com/TryGhost/Product/issues/3832 - Added API error classes and a generic error handling function - Added retry logic to API requests matching the old admin - Added the error handler to all queries and mutations
This commit is contained in:
parent
96ecc73b17
commit
0625255a17
42 changed files with 620 additions and 251 deletions
|
@ -8,6 +8,7 @@ import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel';
|
|||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {ReactComponent as AmpIcon} from '../../../assets/icons/amp.svg';
|
||||
import {ReactComponent as FirstPromoterIcon} from '../../../assets/icons/firstpromoter.svg';
|
||||
|
@ -172,12 +173,16 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio
|
|||
okColor: 'red',
|
||||
okLabel: 'Delete Integration',
|
||||
onOk: async (confirmModal) => {
|
||||
await deleteIntegration(integration.id);
|
||||
confirmModal?.remove();
|
||||
showToast({
|
||||
message: 'Integration deleted',
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
await deleteIntegration(integration.id);
|
||||
confirmModal?.remove();
|
||||
showToast({
|
||||
message: 'Integration deleted',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||
import {RoutingModalProps} from '../../../providers/RoutingProvider';
|
||||
|
@ -40,9 +41,13 @@ const AddIntegrationModal: React.FC<RoutingModalProps> = () => {
|
|||
testId='add-integration-modal'
|
||||
title='Add integration'
|
||||
onOk={async () => {
|
||||
const data = await createIntegration({name});
|
||||
modal.remove();
|
||||
updateRoute({route: `integrations/show/${data.integrations[0].id}`});
|
||||
try {
|
||||
const data = await createIntegration({name});
|
||||
modal.remove();
|
||||
updateRoute({route: `integrations/show/${data.integrations[0].id}`});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='mt-5'>
|
||||
|
|
|
@ -4,6 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as Icon} from '../../../../assets/icons/amp.svg';
|
||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||
|
@ -30,7 +31,11 @@ const AmpModal = NiceModal.create(() => {
|
|||
{key: 'amp', value: enabled},
|
||||
{key: 'amp_gtag_id', value: trackingId}
|
||||
];
|
||||
await editSettings(updates);
|
||||
try {
|
||||
await editSettings(updates);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -81,4 +86,4 @@ const AmpModal = NiceModal.create(() => {
|
|||
);
|
||||
});
|
||||
|
||||
export default AmpModal;
|
||||
export default AmpModal;
|
||||
|
|
|
@ -7,6 +7,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import WebhooksTable from './WebhooksTable';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {APIKey, useRefreshAPIKey} from '../../../../api/apiKeys';
|
||||
|
@ -30,6 +31,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
|||
onSave: async () => {
|
||||
await editIntegration(formState);
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
|
@ -64,9 +66,13 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
|||
prompt: `You can regenerate ${name} API Key any time, but any scripts or applications using it will need to be updated.`,
|
||||
okLabel: `Regenerate ${name} API Key`,
|
||||
onOk: async (confirmModal) => {
|
||||
await refreshAPIKey({integrationId: integration.id, apiKeyId: apiKey.id});
|
||||
setRegenerated(true);
|
||||
confirmModal?.remove();
|
||||
try {
|
||||
await refreshAPIKey({integrationId: integration.id, apiKeyId: apiKey.id});
|
||||
setRegenerated(true);
|
||||
confirmModal?.remove();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -104,8 +110,12 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
|||
width='100px'
|
||||
onDelete={() => updateForm(state => ({...state, icon_image: null}))}
|
||||
onUpload={async (file) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateForm(state => ({...state, icon_image: imageUrl}));
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateForm(state => ({...state, icon_image: imageUrl}));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload icon
|
||||
|
|
|
@ -4,6 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as Icon} from '../../../../assets/icons/firstpromoter.svg';
|
||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||
|
@ -53,9 +54,13 @@ const FirstpromoterModal = NiceModal.create(() => {
|
|||
testId='firstpromoter-modal'
|
||||
title=''
|
||||
onOk={async () => {
|
||||
await handleSave();
|
||||
updateRoute('integrations');
|
||||
modal.remove();
|
||||
try {
|
||||
await handleSave();
|
||||
updateRoute('integrations');
|
||||
modal.remove();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IntegrationHeader
|
||||
|
|
|
@ -4,6 +4,7 @@ import IntegrationHeader from './IntegrationHeader';
|
|||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import pinturaScreenshot from '../../../../assets/images/pintura-screenshot.png';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as Icon} from '../../../../assets/icons/pintura.svg';
|
||||
|
@ -68,10 +69,7 @@ const PinturaModal = NiceModal.create(() => {
|
|||
});
|
||||
} catch (e) {
|
||||
setUploadingState({js: false, css: false});
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: `Can't upload Pintura ${form}!`
|
||||
});
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import IntegrationHeader from './IntegrationHeader';
|
|||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
|
||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||
|
@ -19,7 +20,11 @@ const UnsplashModal = NiceModal.create(() => {
|
|||
const updates: Setting[] = [
|
||||
{key: 'unsplash', value: (e.target.checked)}
|
||||
];
|
||||
await editSettings(updates);
|
||||
try {
|
||||
await editSettings(updates);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
|||
import React from 'react';
|
||||
import Select from '../../../../admin-x-ds/global/form/Select';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import toast from 'react-hot-toast';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import validator from 'validator';
|
||||
|
@ -30,6 +31,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
|||
await createWebhook({...formState, integration_id: integrationId});
|
||||
}
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import TableCell from '../../../../admin-x-ds/global/TableCell';
|
|||
import TableHead from '../../../../admin-x-ds/global/TableHead';
|
||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import WebhookModal from './WebhookModal';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {Integration} from '../../../../api/integrations';
|
||||
import {getWebhookEventLabel} from './webhookEventOptions';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
|
@ -21,12 +22,16 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => {
|
|||
okColor: 'red',
|
||||
okLabel: 'Delete Webhook',
|
||||
onOk: async (confirmModal) => {
|
||||
await deleteWebhook(id);
|
||||
confirmModal?.remove();
|
||||
showToast({
|
||||
message: 'Webhook deleted',
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
await deleteWebhook(id);
|
||||
confirmModal?.remove();
|
||||
showToast({
|
||||
message: 'Webhook deleted',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import List from '../../../../admin-x-ds/global/List';
|
|||
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as ArrowRightIcon} from '../../../../admin-x-ds/assets/icons/arrow-right.svg';
|
||||
import {ReactComponent as Icon} from '../../../../assets/icons/zapier.svg';
|
||||
|
@ -57,9 +58,13 @@ const ZapierModal = NiceModal.create(() => {
|
|||
prompt: 'You will need to locate the Ghost App within your Zapier account and click on "Reconnect" to enter the new Admin API Key.',
|
||||
okLabel: 'Regenerate Admin API Key',
|
||||
onOk: async (confirmModal) => {
|
||||
await refreshAPIKey({integrationId: integration.id, apiKeyId: adminApiKey.id});
|
||||
setRegenerated(true);
|
||||
confirmModal?.remove();
|
||||
try {
|
||||
await refreshAPIKey({integrationId: integration.id, apiKeyId: adminApiKey.id});
|
||||
setRegenerated(true);
|
||||
confirmModal?.remove();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import FileUpload from '../../../../admin-x-ds/global/form/FileUpload';
|
|||
import LabItem from './LabItem';
|
||||
import List from '../../../../admin-x-ds/global/List';
|
||||
import React, {useState} from 'react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
|
||||
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
|
||||
|
@ -35,13 +36,18 @@ const BetaFeatures: React.FC = () => {
|
|||
<FileUpload
|
||||
id='upload-redirects'
|
||||
onUpload={async (file) => {
|
||||
setRedirectsUploading(true);
|
||||
await uploadRedirects(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Redirects uploaded successfully'
|
||||
});
|
||||
setRedirectsUploading(false);
|
||||
try {
|
||||
setRedirectsUploading(true);
|
||||
await uploadRedirects(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Redirects uploaded successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setRedirectsUploading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button color='grey' label={redirectsUploading ? 'Uploading ...' : 'Upload redirects file'} size='sm' tag='div' />
|
||||
|
@ -55,13 +61,18 @@ const BetaFeatures: React.FC = () => {
|
|||
<FileUpload
|
||||
id='upload-routes'
|
||||
onUpload={async (file) => {
|
||||
setRoutesUploading(true);
|
||||
await uploadRoutes(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Routes uploaded successfully'
|
||||
});
|
||||
setRoutesUploading(false);
|
||||
try {
|
||||
setRoutesUploading(true);
|
||||
await uploadRoutes(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Routes uploaded successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setRoutesUploading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button color='grey' label={routesUploading ? 'Uploading ...' : 'Upload routes file'} size='sm' tag='div' />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {ConfigResponseType, configDataType} from '../../../../api/config';
|
||||
import {getSettingValue, useEditSettings} from '../../../../api/settings';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
|
@ -15,20 +16,24 @@ const FeatureToggle: React.FC<{ flag: string; }> = ({flag}) => {
|
|||
|
||||
return <Toggle checked={labs[flag]} onChange={async () => {
|
||||
const newValue = !labs[flag];
|
||||
await editSettings([{
|
||||
key: 'labs',
|
||||
value: JSON.stringify({...labs, [flag]: newValue})
|
||||
}]);
|
||||
toggleFeatureFlag(flag, newValue);
|
||||
client.setQueriesData([configDataType], current => ({
|
||||
config: {
|
||||
...(current as ConfigResponseType).config,
|
||||
labs: {
|
||||
...(current as ConfigResponseType).config.labs,
|
||||
[flag]: newValue
|
||||
try {
|
||||
await editSettings([{
|
||||
key: 'labs',
|
||||
value: JSON.stringify({...labs, [flag]: newValue})
|
||||
}]);
|
||||
toggleFeatureFlag(flag, newValue);
|
||||
client.setQueriesData([configDataType], current => ({
|
||||
config: {
|
||||
...(current as ConfigResponseType).config,
|
||||
labs: {
|
||||
...(current as ConfigResponseType).config.labs,
|
||||
[flag]: newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import LabItem from './LabItem';
|
|||
import List from '../../../../admin-x-ds/global/List';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {downloadAllContent, useDeleteAllContent, useImportContent} from '../../../../api/db';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import {useQueryClient} from '@tanstack/react-query';
|
||||
|
@ -18,16 +19,22 @@ const ImportModalContent = () => {
|
|||
id="import-file"
|
||||
onUpload={async (file) => {
|
||||
setUploading(true);
|
||||
await importContent(file);
|
||||
modal.remove();
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Import in progress',
|
||||
prompt: `Your import is being processed, and you'll receive a confirmation email as soon as it's complete. Usually this only takes a few minutes, but larger imports may take longer.`,
|
||||
cancelLabel: '',
|
||||
okLabel: 'Got it',
|
||||
onOk: confirmModal => confirmModal?.remove(),
|
||||
formSheet: false
|
||||
});
|
||||
try {
|
||||
await importContent(file);
|
||||
modal.remove();
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Import in progress',
|
||||
prompt: `Your import is being processed, and you'll receive a confirmation email as soon as it's complete. Usually this only takes a few minutes, but larger imports may take longer.`,
|
||||
cancelLabel: '',
|
||||
okLabel: 'Got it',
|
||||
onOk: confirmModal => confirmModal?.remove(),
|
||||
formSheet: false
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="cursor-pointer bg-grey-75 p-10 text-center dark:bg-grey-950">
|
||||
|
@ -56,12 +63,16 @@ const MigrationOptions: React.FC = () => {
|
|||
okColor: 'red',
|
||||
okLabel: 'Delete',
|
||||
onOk: async () => {
|
||||
await deleteAllContent(null);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'All content deleted from database.'
|
||||
});
|
||||
await client.refetchQueries();
|
||||
try {
|
||||
await deleteAllContent(null);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'All content deleted from database.'
|
||||
});
|
||||
await client.refetchQueries();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
@ -27,7 +28,11 @@ const EnableNewsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updates.push({key: 'editor_default_email_recipients_filter', value: null});
|
||||
}
|
||||
|
||||
await editSettings(updates);
|
||||
try {
|
||||
await editSettings(updates);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const enableToggle = (
|
||||
|
|
|
@ -5,6 +5,7 @@ import Select from '../../../admin-x-ds/global/form/Select';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {getSettingValues, useEditSettings} from '../../../api/settings';
|
||||
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
|
||||
|
@ -113,7 +114,12 @@ const MailGun: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
// resulting in the mailgun base url remaining null
|
||||
// this should not fire if the user has changed the region or if the region is already set
|
||||
if (!mailgunRegion) {
|
||||
await editSettings([{key: 'mailgun_base_url', value: MAILGUN_REGIONS[0].value}]);
|
||||
try {
|
||||
await editSettings([{key: 'mailgun_base_url', value: MAILGUN_REGIONS[0].value}]);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleSave();
|
||||
}}
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, {useEffect} from 'react';
|
|||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||
|
@ -39,6 +40,7 @@ const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
|
|||
|
||||
updateRoute({route: `newsletters/show/${response.newsletters[0].id}`});
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
|||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
|
@ -84,12 +85,16 @@ const Sidebar: React.FC<{
|
|||
okLabel: 'Archive',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
await editNewsletter({...newsletter, status: 'archived'});
|
||||
modal?.remove();
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Newsletter archived successfully'
|
||||
});
|
||||
try {
|
||||
await editNewsletter({...newsletter, status: 'archived'});
|
||||
modal?.remove();
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Newsletter archived successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -212,8 +217,12 @@ const Sidebar: React.FC<{
|
|||
updateNewsletter({header_image: null});
|
||||
}}
|
||||
onUpload={async (file) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateNewsletter({header_image: imageUrl});
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateNewsletter({header_image: imageUrl});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon colorClass='text-grey-700 dark:text-grey-300' name='picture' />
|
||||
|
@ -447,6 +456,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
|||
modal.remove();
|
||||
}
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
|
|
|
@ -13,52 +13,7 @@ interface NewslettersListProps {
|
|||
}
|
||||
|
||||
const NewsletterItem: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {
|
||||
// const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
const {updateRoute} = useRouting();
|
||||
// const limiter = useLimiter();
|
||||
// const action = newsletter.status === 'active' ? (
|
||||
// <Button color='red' disabled={onlyOne} label='Archive' link onClick={() => {
|
||||
// NiceModal.show(ConfirmationModal, {
|
||||
// title: 'Archive newsletter',
|
||||
// prompt: <>
|
||||
// <p>Your newsletter <strong>{newsletter.name}</strong> will no longer be visible to members or available as an option when publishing new posts.</p>
|
||||
// <p>Existing posts previously sent as this newsletter will remain unchanged.</p>
|
||||
// </>,
|
||||
// okLabel: 'Archive',
|
||||
// onOk: async (modal) => {
|
||||
// await editNewsletter({...newsletter, status: 'archived'});
|
||||
// modal?.remove();
|
||||
// }
|
||||
// });
|
||||
// }} />
|
||||
// ) : (
|
||||
// <Button color='green' label='Activate' link onClick={async () => {
|
||||
// try {
|
||||
// await limiter?.errorIfWouldGoOverLimit('newsletters');
|
||||
// } catch (error) {
|
||||
// if (error instanceof HostLimitError) {
|
||||
// NiceModal.show(LimitModal, {
|
||||
// prompt: error.message || `Your current plan doesn't support more newsletters.`
|
||||
// });
|
||||
// return;
|
||||
// } else {
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
|
||||
// NiceModal.show(ConfirmationModal, {
|
||||
// title: 'Reactivate newsletter',
|
||||
// prompt: <>
|
||||
// Reactivating <strong>{newsletter.name}</strong> will immediately make it visible to members and re-enable it as an option when publishing new posts.
|
||||
// </>,
|
||||
// okLabel: 'Reactivate',
|
||||
// onOk: async (modal) => {
|
||||
// await editNewsletter({...newsletter, status: 'active'});
|
||||
// modal?.remove();
|
||||
// }
|
||||
// });
|
||||
// }} />
|
||||
// );
|
||||
|
||||
const showDetails = () => {
|
||||
updateRoute({route: `newsletters/show/${newsletter.id}`});
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg';
|
||||
|
@ -52,8 +53,12 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
};
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('og_image', imageUrl);
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('og_image', imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageDelete = () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import Modal from '../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Radio from '../../../admin-x-ds/global/form/Radio';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import validator from 'validator';
|
||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||
|
@ -119,6 +120,7 @@ const InviteUserModal = NiceModal.create(() => {
|
|||
message: `Failed to send invitation to ${email}`,
|
||||
type: 'error'
|
||||
});
|
||||
handleError(e, {withToast: false});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg';
|
||||
|
@ -53,8 +54,8 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('twitter_image', imageUrl);
|
||||
} catch (err) {
|
||||
// TODO: handle error
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
|
|||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import clsx from 'clsx';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
|
@ -278,9 +279,13 @@ const StaffToken: React.FC<UserDetailProps> = () => {
|
|||
okLabel: 'Regenerate your Staff Access Token',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
const newAPI = await newApiKey([]);
|
||||
setToken(newAPI?.apiKey?.secret || '');
|
||||
modal?.remove();
|
||||
try {
|
||||
const newAPI = await newApiKey([]);
|
||||
setToken(newAPI?.apiKey?.secret || '');
|
||||
modal?.remove();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -395,13 +400,17 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||
..._user,
|
||||
status: _user.status === 'inactive' ? 'active' : 'inactive'
|
||||
};
|
||||
await updateUser(updatedUserData);
|
||||
setUserData(updatedUserData);
|
||||
modal?.remove();
|
||||
showToast({
|
||||
message: _user.status === 'inactive' ? 'User un-suspended' : 'User suspended',
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
await updateUser(updatedUserData);
|
||||
setUserData(updatedUserData);
|
||||
modal?.remove();
|
||||
showToast({
|
||||
message: _user.status === 'inactive' ? 'User un-suspended' : 'User suspended',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -418,13 +427,17 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||
okLabel: 'Delete user',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
await deleteUser(_user?.id);
|
||||
modal?.remove();
|
||||
mainModal?.remove();
|
||||
showToast({
|
||||
message: 'User deleted',
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
await deleteUser(_user?.id);
|
||||
modal?.remove();
|
||||
mainModal?.remove();
|
||||
showToast({
|
||||
message: 'User deleted',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -436,12 +449,16 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||
okLabel: 'Yep — I\'m sure',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
await makeOwner(user.id);
|
||||
modal?.remove();
|
||||
showToast({
|
||||
message: 'Ownership transferred',
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
await makeOwner(user.id);
|
||||
modal?.remove();
|
||||
showToast({
|
||||
message: 'Ownership transferred',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -462,8 +479,8 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: handle error
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import React, {useState} from 'react';
|
|||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import clsx from 'clsx';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import {User, hasAdminAccess, isContributorUser, isEditorUser} from '../../../api/users';
|
||||
|
@ -127,13 +128,18 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => {
|
|||
label={revokeActionLabel}
|
||||
link={true}
|
||||
onClick={async () => {
|
||||
setRevokeState('progress');
|
||||
await deleteInvite(invite.id);
|
||||
setRevokeState('');
|
||||
showToast({
|
||||
message: `Invitation revoked (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
setRevokeState('progress');
|
||||
await deleteInvite(invite.id);
|
||||
showToast({
|
||||
message: `Invitation revoked (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setRevokeState('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
|
@ -142,17 +148,22 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => {
|
|||
label={resendActionLabel}
|
||||
link={true}
|
||||
onClick={async () => {
|
||||
setResendState('progress');
|
||||
await deleteInvite(invite.id);
|
||||
await addInvite({
|
||||
email: invite.email,
|
||||
roleId: invite.role_id
|
||||
});
|
||||
setResendState('');
|
||||
showToast({
|
||||
message: `Invitation resent! (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
try {
|
||||
setResendState('progress');
|
||||
await deleteInvite(invite.id);
|
||||
await addInvite({
|
||||
email: invite.email,
|
||||
roleId: invite.role_id
|
||||
});
|
||||
showToast({
|
||||
message: `Invitation resent! (${invite.email})`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setResendState('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import Button from '../../../../admin-x-ds/global/Button';
|
|||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {User, useUpdatePassword} from '../../../../api/users';
|
||||
import {ValidationError} from '../../../../utils/errors';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
|
@ -223,6 +224,7 @@ const ChangePasswordForm: React.FC<{user: User}> = ({user}) => {
|
|||
type: 'pageError',
|
||||
message: e instanceof ValidationError ? e.message : `Couldn't update password. Please try again.`
|
||||
});
|
||||
handleError(e, {withToast: false});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -7,6 +7,7 @@ import Select from '../../../../admin-x-ds/global/form/Select';
|
|||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import clsx from 'clsx';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {ReactComponent as PortalIcon1} from '../../../../assets/icons/portal-icon-1.svg';
|
||||
import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
|
||||
import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg';
|
||||
|
@ -52,9 +53,13 @@ const LookAndFeel: React.FC<{
|
|||
const [uploadedIcon, setUploadedIcon] = useState(isDefaultIcon ? undefined : currentIcon);
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('portal_button_icon', imageUrl);
|
||||
setUploadedIcon(imageUrl);
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('portal_button_icon', imageUrl);
|
||||
setUploadedIcon(imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageDelete = () => {
|
||||
|
|
|
@ -6,14 +6,14 @@ import PortalPreview from './PortalPreview';
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import SignupOptions from './SignupOptions';
|
||||
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm, {Dirtyable} from '../../../../hooks/useForm';
|
||||
import useQueryParams from '../../../../hooks/useQueryParams';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {Setting, SettingValue, useEditSettings} from '../../../../api/settings';
|
||||
import {Setting, SettingValue, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||
import {Tier, getPaidActiveTiers, useBrowseTiers, useEditTier} from '../../../../api/tiers';
|
||||
import {fullEmailAddress} from '../../../../api/site';
|
||||
import {getSettingValues} from '../../../../api/settings';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
import {verifyEmailToken} from '../../../../api/emailVerification';
|
||||
|
||||
|
@ -106,6 +106,7 @@ const PortalModal: React.FC = () => {
|
|||
cancelLabel: '',
|
||||
onOk: confirmModal => confirmModal?.remove()
|
||||
});
|
||||
handleError(e, {withToast: false});
|
||||
}
|
||||
};
|
||||
if (verifyEmail) {
|
||||
|
@ -139,7 +140,9 @@ const PortalModal: React.FC = () => {
|
|||
onOk: confirmModal => confirmModal?.remove()
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onSaveError: handleError
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||
|
|
|
@ -13,6 +13,7 @@ import StripeLogo from '../../../../assets/images/stripe-emblem.svg';
|
|||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import {JSONError} from '../../../../utils/errors';
|
||||
|
@ -86,7 +87,8 @@ const Connect: React.FC = () => {
|
|||
// no-op: will try saving again as stripe is not ready
|
||||
continue;
|
||||
} else {
|
||||
throw e;
|
||||
handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,8 +113,10 @@ const Connect: React.FC = () => {
|
|||
if (e instanceof JSONError && e.data?.errors) {
|
||||
setError('Invalid secure key');
|
||||
return;
|
||||
} else {
|
||||
handleError(e);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
setError('Please enter a secure key');
|
||||
|
@ -169,9 +173,13 @@ const Connected: React.FC<{onClose?: () => void}> = ({onClose}) => {
|
|||
from this site. This will automatically turn off paid memberships on this site.</>),
|
||||
okLabel: hasActiveStripeSubscriptions ? '' : 'Disconnect',
|
||||
onOk: async (modal) => {
|
||||
await deleteStripeSettings(null);
|
||||
modal?.remove();
|
||||
onClose?.();
|
||||
try {
|
||||
await deleteStripeSettings(null);
|
||||
modal?.remove();
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import SortableList from '../../../../admin-x-ds/global/SortableList';
|
|||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import TierDetailPreview from './TierDetailPreview';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
|
@ -69,7 +70,8 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||
}
|
||||
|
||||
modal.remove();
|
||||
}
|
||||
},
|
||||
onSaveError: handleError
|
||||
});
|
||||
|
||||
const validators = {
|
||||
|
|
|
@ -7,6 +7,7 @@ import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
|
|||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '../../../api/customThemeSettings';
|
||||
|
@ -116,7 +117,8 @@ const DesignModal: React.FC = () => {
|
|||
const {settings: newSettings} = await editSettings(formState.settings.filter(setting => setting.dirty));
|
||||
updateForm(state => ({...state, settings: newSettings}));
|
||||
}
|
||||
}
|
||||
},
|
||||
onSaveError: handleError
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -12,9 +12,10 @@ import React, {useEffect, useState} from 'react';
|
|||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import ThemeInstalledModal from './theme/ThemeInstalledModal';
|
||||
import ThemePreview from './theme/ThemePreview';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||
import {InstalledTheme, Theme, useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../api/themes';
|
||||
import {InstalledTheme, Theme, ThemesInstallResponseType, useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../api/themes';
|
||||
import {OfficialTheme} from '../../providers/ServiceProvider';
|
||||
|
||||
interface ThemeToolbarProps {
|
||||
|
@ -71,7 +72,18 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
|||
file: File;
|
||||
onActivate?: () => void
|
||||
}) => {
|
||||
const data = await uploadTheme({file});
|
||||
let data: ThemesInstallResponseType | undefined;
|
||||
|
||||
try {
|
||||
data = await uploadTheme({file});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadedTheme = data.themes[0];
|
||||
|
||||
let title = 'Upload successful';
|
||||
|
@ -247,8 +259,18 @@ const ChangeThemeModal = () => {
|
|||
prompt = <>By clicking below, <strong>{selectedTheme.name}</strong> will automatically be activated as the theme for your site.</>;
|
||||
} else {
|
||||
setInstalling(true);
|
||||
const data = await installTheme(selectedTheme.ref);
|
||||
setInstalling(false);
|
||||
let data: ThemesInstallResponseType | undefined;
|
||||
try {
|
||||
data = await installTheme(selectedTheme.ref);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newlyInstalledTheme = data.themes[0];
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, {useRef, useState} from 'react';
|
|||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import UnsplashSearchModal from '../../../../utils/unsplash/UnsplashSearchModal';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||
import {SettingValue, getSettingValues} from '../../../../api/settings';
|
||||
import {debounce} from '../../../../utils/debounce';
|
||||
|
@ -91,7 +92,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
width={values.icon ? '66px' : '150px'}
|
||||
onDelete={() => updateSetting('icon', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('icon', getImageUrl(await uploadImage({file})));
|
||||
try {
|
||||
updateSetting('icon', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload icon
|
||||
|
@ -109,7 +114,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
imageURL={values.logo || ''}
|
||||
onDelete={() => updateSetting('logo', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('logo', getImageUrl(await uploadImage({file})));
|
||||
try {
|
||||
updateSetting('logo', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload logo
|
||||
|
@ -130,7 +139,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
openEditor: async () => editor.openEditor({
|
||||
image: values.coverImage || '',
|
||||
handleSave: async (file:File) => {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
try {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -139,7 +152,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
|||
unsplashEnabled={true}
|
||||
onDelete={() => updateSetting('cover_image', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
try {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload cover
|
||||
|
|
|
@ -7,6 +7,7 @@ import Select from '../../../../admin-x-ds/global/form/Select';
|
|||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
|
||||
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
||||
import {humanizeSettingKey} from '../../../../api/settings';
|
||||
|
@ -18,8 +19,12 @@ const ThemeSetting: React.FC<{
|
|||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
setSetting(imageUrl);
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
setSetting(imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
switch (setting.type) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {EditOrAddRecommendation, useAddRecommendation} from '../../../../api/recommendations';
|
||||
|
@ -31,6 +32,7 @@ const AddRecommendationModalConfirm: React.FC<AddRecommendationModalProps> = ({r
|
|||
});
|
||||
updateRoute('recommendations');
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!formState.title) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Recommendation, useDeleteRecommendation, useEditRecommendation} from '../../../../api/recommendations';
|
||||
|
@ -30,6 +31,7 @@ const EditRecommendationModal: React.FC<RoutingModalProps & EditRecommendationMo
|
|||
modal.remove();
|
||||
updateRoute('recommendations');
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!formState.title) {
|
||||
|
@ -68,11 +70,12 @@ const EditRecommendationModal: React.FC<RoutingModalProps & EditRecommendationMo
|
|||
message: 'Successfully deleted the recommendation',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (_) {
|
||||
} catch (e) {
|
||||
showToast({
|
||||
message: 'Failed to delete the recommendation. Please try again later.',
|
||||
type: 'error'
|
||||
});
|
||||
handleError(e, {withToast: false});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import Menu from '../../../../admin-x-ds/global/Menu';
|
|||
import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
|
||||
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
|
||||
|
||||
|
@ -50,7 +51,11 @@ const ThemeActions: React.FC<ThemeActionProps> = ({
|
|||
const {mutateAsync: deleteTheme} = useDeleteTheme();
|
||||
|
||||
const handleActivate = async () => {
|
||||
await activateTheme(theme.name);
|
||||
try {
|
||||
await activateTheme(theme.name);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
|
@ -80,8 +85,12 @@ const ThemeActions: React.FC<ThemeActionProps> = ({
|
|||
okRunningLabel: 'Deleting',
|
||||
okColor: 'red',
|
||||
onOk: async (modal) => {
|
||||
await deleteTheme(theme.name);
|
||||
modal?.remove();
|
||||
try {
|
||||
await deleteTheme(theme.name);
|
||||
modal?.remove();
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import List from '../../../../admin-x-ds/global/List';
|
|||
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {ReactNode, useState} from 'react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import {ConfirmationModalContent} from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||
import {InstalledTheme, ThemeProblem, useActivateTheme} from '../../../../api/themes';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
|
@ -85,13 +86,17 @@ const ThemeInstalledModal: React.FC<{
|
|||
title={title}
|
||||
onOk={async (activateModal) => {
|
||||
if (!installedTheme.active) {
|
||||
const resData = await activateTheme(installedTheme.name);
|
||||
const updatedTheme = resData.themes[0];
|
||||
try {
|
||||
const resData = await activateTheme(installedTheme.name);
|
||||
const updatedTheme = resData.themes[0];
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: <div><span className='capitalize'>{updatedTheme.name}</span> is now your active theme.</div>
|
||||
});
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: <div><span className='capitalize'>{updatedTheme.name}</span> is now your active theme.</div>
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
onActivate?.();
|
||||
activateModal?.remove();
|
||||
|
|
|
@ -31,9 +31,10 @@ export interface FormHook<State> {
|
|||
errors: ErrorMessages;
|
||||
}
|
||||
|
||||
const useForm = <State>({initialState, onSave, onValidate}: {
|
||||
const useForm = <State>({initialState, onSave, onSaveError, onValidate}: {
|
||||
initialState: State,
|
||||
onSave: () => void | Promise<void>
|
||||
onSaveError?: (error: unknown) => void | Promise<void>
|
||||
onValidate?: () => ErrorMessages
|
||||
}): FormHook<State> => {
|
||||
const [formState, setFormState] = useState(initialState);
|
||||
|
@ -77,6 +78,7 @@ const useForm = <State>({initialState, onSave, onValidate}: {
|
|||
setSaveState('saved');
|
||||
return true;
|
||||
} catch (e) {
|
||||
await onSaveError?.(e);
|
||||
setSaveState('unsaved');
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import handleError from '../utils/handleError';
|
||||
import useForm, {ErrorMessages, SaveState} from './useForm';
|
||||
import useGlobalDirtyState from './useGlobalDirtyState';
|
||||
import {Setting, SettingValue, useEditSettings} from '../api/settings';
|
||||
|
@ -40,6 +41,7 @@ const useSettingGroup = ({onValidate}: {onValidate?: () => ErrorMessages} = {}):
|
|||
onSave: async () => {
|
||||
await editSettings?.(changedSettings());
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import handleError from './handleError';
|
||||
import handleResponse from './handleResponse';
|
||||
import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from './errors';
|
||||
import {QueryClient, UseInfiniteQueryOptions, UseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {getGhostPaths} from './helpers';
|
||||
import {useMemo} from 'react';
|
||||
import {useEffect, useMemo} from 'react';
|
||||
import {usePage, usePagination} from '../hooks/usePagination';
|
||||
import {useServices} from '../components/providers/ServiceProvider';
|
||||
|
||||
|
@ -47,27 +49,77 @@ export const useFetchApi = () => {
|
|||
setTimeout(() => controller.abort(), timeout);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...headers
|
||||
},
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
...options
|
||||
});
|
||||
// attempt retries for 15 seconds in two situations:
|
||||
// 1. Server Unreachable error from the browser (code 0 or TypeError), typically from short internet blips
|
||||
// 2. Maintenance error from Ghost, upgrade in progress so API is temporarily unavailable
|
||||
let attempts = 0;
|
||||
let retryingMs = 0;
|
||||
const startTime = Date.now();
|
||||
const maxRetryingMs = 15_000;
|
||||
const retryPeriods = [500, 1000];
|
||||
const retryableErrors = [ServerUnreachableError, MaintenanceError, TypeError];
|
||||
|
||||
return handleResponse(response);
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
// const getErrorData = (error?: APIError, response?: Response) => {
|
||||
// const data: Record<string, unknown> = {
|
||||
// errorName: error?.name,
|
||||
// attempts,
|
||||
// totalSeconds: retryingMs / 1000
|
||||
// };
|
||||
// if (endpoint.toString().includes('/ghost/api/')) {
|
||||
// data.server = response?.headers.get('server');
|
||||
// }
|
||||
// return data;
|
||||
// };
|
||||
|
||||
throw error;
|
||||
};
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...headers
|
||||
},
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
signal: controller.signal,
|
||||
...options
|
||||
});
|
||||
|
||||
// TODO: Add Sentry integration
|
||||
// if (attempts !== 0 && config.sentry_dsn) {
|
||||
// captureMessage('Request took multiple attempts', {extra: getErrorData()});
|
||||
// }
|
||||
|
||||
return handleResponse(response);
|
||||
} catch (error) {
|
||||
retryingMs = Date.now() - startTime;
|
||||
|
||||
if (import.meta.env.MODE !== 'development' && retryableErrors.some(errorClass => error instanceof errorClass) && retryingMs <= maxRetryingMs) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, retryPeriods[attempts] || retryPeriods[retryPeriods.length - 1]);
|
||||
});
|
||||
attempts += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Add Sentry integration
|
||||
// if (attempts > 0 && config.sentry_dsn) {
|
||||
// captureMessage('Request failed after multiple attempts', {extra: getErrorData()});
|
||||
// }
|
||||
|
||||
if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
|
||||
throw new TimeoutError();
|
||||
}
|
||||
|
||||
let newError = error;
|
||||
|
||||
if (!(error instanceof APIError)) {
|
||||
newError = new ServerUnreachableError({cause: error});
|
||||
}
|
||||
|
||||
throw newError;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -91,7 +143,10 @@ interface QueryOptions<ResponseData> {
|
|||
returnData?: (originalData: unknown) => ResponseData;
|
||||
}
|
||||
|
||||
type QueryHookOptions<ResponseData> = UseQueryOptions<ResponseData> & { searchParams?: Record<string, string> };
|
||||
type QueryHookOptions<ResponseData> = UseQueryOptions<ResponseData> & {
|
||||
searchParams?: Record<string, string>;
|
||||
defaultErrorHandler?: boolean;
|
||||
};
|
||||
|
||||
export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) => ({searchParams, ...query}: QueryHookOptions<ResponseData> = {}) => {
|
||||
const url = apiUrl(options.path, searchParams || options.defaultSearchParams);
|
||||
|
@ -107,6 +162,12 @@ export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) =
|
|||
(result.data && options.returnData) ? options.returnData(result.data) : result.data)
|
||||
, [result]);
|
||||
|
||||
useEffect(() => {
|
||||
if (result.error && query.defaultErrorHandler !== false) {
|
||||
handleError(result.error);
|
||||
}
|
||||
}, [result.error, query.defaultErrorHandler]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data
|
||||
|
@ -141,6 +202,12 @@ export const createPaginatedQuery = <ResponseData extends {meta?: Meta}>(options
|
|||
meta: result.isFetching ? undefined : data?.meta
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (result.error && query.defaultErrorHandler !== false) {
|
||||
handleError(result.error);
|
||||
}
|
||||
}, [result.error, query.defaultErrorHandler]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data,
|
||||
|
@ -154,6 +221,7 @@ type InfiniteQueryOptions<ResponseData> = Omit<QueryOptions<ResponseData>, 'retu
|
|||
|
||||
type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & {
|
||||
searchParams?: Record<string, string>;
|
||||
defaultErrorHandler?: boolean;
|
||||
getNextPageParams: (data: ResponseData, params: Record<string, string>) => Record<string, string>|undefined;
|
||||
};
|
||||
|
||||
|
@ -169,6 +237,12 @@ export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<
|
|||
|
||||
const data = useMemo(() => result.data && options.returnData(result.data), [result]);
|
||||
|
||||
useEffect(() => {
|
||||
if (result.error && query.defaultErrorHandler !== false) {
|
||||
handleError(result.error);
|
||||
}
|
||||
}, [result.error, query.defaultErrorHandler]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
interface ErrorResponse {
|
||||
// API errors
|
||||
|
||||
export interface ErrorResponse {
|
||||
errors: Array<{
|
||||
code: string
|
||||
context: string | null
|
||||
|
@ -14,11 +16,12 @@ interface ErrorResponse {
|
|||
|
||||
export class APIError extends Error {
|
||||
constructor(
|
||||
public readonly response: Response,
|
||||
public readonly response?: Response,
|
||||
public readonly data?: unknown,
|
||||
message?: string
|
||||
message?: string,
|
||||
errorOptions?: ErrorOptions
|
||||
) {
|
||||
super(message || response.statusText);
|
||||
super(message || response?.statusText, errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,18 +29,77 @@ export class JSONError extends APIError {
|
|||
constructor(
|
||||
response: Response,
|
||||
public readonly data?: ErrorResponse,
|
||||
message?: string
|
||||
message?: string,
|
||||
errorOptions?: ErrorOptions
|
||||
) {
|
||||
super(response, data, message);
|
||||
super(response, data, message, errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class VersionMismatchError extends JSONError {
|
||||
constructor(response: Response, data: ErrorResponse, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'API server is running a newer version of Ghost, please upgrade.', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerUnreachableError extends APIError {
|
||||
constructor(errorOptions?: ErrorOptions) {
|
||||
super(undefined, undefined, 'Server was unreachable', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutError extends APIError {
|
||||
constructor(errorOptions?: ErrorOptions) {
|
||||
super(undefined, undefined, 'Request timed out', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestEntityTooLargeError extends APIError {
|
||||
constructor(response: Response, data: unknown, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'Request is larger than the maximum file size the server allows', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedMediaTypeError extends APIError {
|
||||
constructor(response: Response, data: unknown, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'Request contains an unknown or unsupported file type.', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaintenanceError extends APIError {
|
||||
constructor(response: Response, data: unknown, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'Ghost is currently undergoing maintenance, please wait a moment then retry.', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeValidationError extends JSONError {
|
||||
constructor(response: Response, data: ErrorResponse, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'Theme is not compatible or contains errors.', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class HostLimitError extends JSONError {
|
||||
constructor(response: Response, data: ErrorResponse, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'A hosting plan limit was reached or exceeded.', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailError extends JSONError {
|
||||
constructor(response: Response, data: ErrorResponse, errorOptions?: ErrorOptions) {
|
||||
super(response, data, 'Please verify your email settings', errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends JSONError {
|
||||
constructor(response: Response, data: ErrorResponse) {
|
||||
super(response, data, data.errors[0].message);
|
||||
constructor(response: Response, data: ErrorResponse, errorOptions?: ErrorOptions) {
|
||||
super(response, data, data.errors[0].message, errorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
export const errorsWithMessage = [ValidationError, ThemeValidationError, HostLimitError, EmailError];
|
||||
|
||||
// Frontend errors
|
||||
|
||||
export class AlreadyExistsError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
|
|
44
apps/admin-x-settings/src/utils/handleError.ts
Normal file
44
apps/admin-x-settings/src/utils/handleError.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import toast from 'react-hot-toast';
|
||||
import {APIError, ValidationError} from './errors';
|
||||
import {showToast} from '../admin-x-ds/global/Toast';
|
||||
|
||||
/**
|
||||
* Generic error handling for API calls. This is enabled by default for queries (can be disabled by
|
||||
* setting defaultErrorHandler:false when using the query) but should be called when mutations throw
|
||||
* errors in order to handle anything unexpected.
|
||||
*
|
||||
* @param error Thrown error.
|
||||
* @param options.withToast Show a toast with the error message (default: true).
|
||||
* In general we should validate on the client side before sending the request to avoid errors,
|
||||
* so this toast is intended as a worst-case fallback message when we don't know what else to do.
|
||||
* @returns
|
||||
*/
|
||||
const handleError = (error: unknown, {withToast = true}: {withToast?: boolean} = {}) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
||||
if (!withToast) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.remove();
|
||||
|
||||
if (error instanceof ValidationError && error.data?.errors[0]) {
|
||||
showToast({
|
||||
message: error.data.errors[0].context || error.data.errors[0].message,
|
||||
type: 'pageError'
|
||||
});
|
||||
} else if (error instanceof APIError) {
|
||||
showToast({
|
||||
message: error.message,
|
||||
type: 'pageError'
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
message: 'Something went wrong, please try again.',
|
||||
type: 'pageError'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handleError;
|
|
@ -1,20 +1,34 @@
|
|||
import {APIError, JSONError, ValidationError} from './errors';
|
||||
import {APIError, EmailError, ErrorResponse, HostLimitError, JSONError, MaintenanceError, RequestEntityTooLargeError, ServerUnreachableError, ThemeValidationError, UnsupportedMediaTypeError, ValidationError, VersionMismatchError} from './errors';
|
||||
|
||||
const handleResponse = async (response: Response) => {
|
||||
if (response.status === 422) {
|
||||
const data = await response.json();
|
||||
if (response.status === 0) {
|
||||
throw new ServerUnreachableError();
|
||||
} else if (response.status === 503) {
|
||||
throw new MaintenanceError(response, await response.text());
|
||||
} else if (response.status === 415) {
|
||||
throw new UnsupportedMediaTypeError(response, await response.text());
|
||||
} else if (response.status === 413) {
|
||||
throw new RequestEntityTooLargeError(response, await response.text());
|
||||
} else if (!response.ok) {
|
||||
if (!response.headers.get('content-type')?.includes('json')) {
|
||||
throw new APIError(response, await response.text());
|
||||
}
|
||||
|
||||
if (data.errors?.[0]?.type === 'ValidationError') {
|
||||
const data = await response.json() as ErrorResponse;
|
||||
|
||||
if (data.errors?.[0]?.type === 'VersionMismatchError') {
|
||||
throw new VersionMismatchError(response, data);
|
||||
} else if (data.errors?.[0]?.type === 'ValidationError') {
|
||||
throw new ValidationError(response, data);
|
||||
} else if (data.errors?.[0]?.type === 'ThemeValidationError') {
|
||||
throw new ThemeValidationError(response, data);
|
||||
} else if (data.errors?.[0]?.type === 'HostLimitError') {
|
||||
throw new HostLimitError(response, data);
|
||||
} else if (data.errors?.[0]?.type === 'EmailError') {
|
||||
throw new EmailError(response, data);
|
||||
} else {
|
||||
throw new JSONError(response, data);
|
||||
}
|
||||
} else if (response.status > 299) {
|
||||
if (response.headers.get('content-type')?.includes('json')) {
|
||||
throw new JSONError(response, await response.json());
|
||||
} else {
|
||||
throw new APIError(response, await response.text());
|
||||
}
|
||||
} else if (response.status === 204) {
|
||||
return;
|
||||
} else if (response.headers.get('content-type')?.includes('text/csv')) {
|
||||
|
|
Loading…
Add table
Reference in a new issue