Updated AdminX error handling to display validation errors correctly (#19210)

fixes ADM-44
This commit is contained in:
Jono M 2023-11-30 16:47:59 +00:00 committed by GitHub
parent 2809703c76
commit cc4176f0bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 14 additions and 25 deletions

View File

@ -3,7 +3,7 @@ import {showToast} from '@tryghost/admin-x-design-system';
import {useCallback} from 'react'; import {useCallback} from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import {useFramework} from '../providers/FrameworkProvider'; import {useFramework} from '../providers/FrameworkProvider';
import {APIError, JSONError, ValidationError} from '../utils/errors'; import {APIError, ValidationError} from '../utils/errors';
/** /**
* Generic error handling for API calls. This is enabled by default for queries (can be disabled by * Generic error handling for API calls. This is enabled by default for queries (can be disabled by
@ -20,8 +20,7 @@ const useHandleError = () => {
* so this toast is intended as a worst-case fallback message when we don't know what else to do. * so this toast is intended as a worst-case fallback message when we don't know what else to do.
* *
*/ */
type HandleErrorReturnType = void | any; // eslint-disable-line @typescript-eslint/no-explicit-any const handleError = useCallback((error: unknown, {withToast = true}: {withToast?: boolean} = {}) => {
const handleError = useCallback((error: unknown, {withToast = true}: {withToast?: boolean} = {}) : HandleErrorReturnType => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
@ -41,10 +40,6 @@ const useHandleError = () => {
toast.remove(); toast.remove();
if (error instanceof JSONError && error.response?.status === 422) {
return error.data;
}
if (error instanceof APIError && error.response?.status === 418) { if (error instanceof APIError && error.response?.status === 418) {
// We use this status in tests to indicate the API request was not mocked - // We use this status in tests to indicate the API request was not mocked -
// don't show a toast because it may block clicking things in the test // don't show a toast because it may block clicking things in the test

View File

@ -1,13 +1,14 @@
import AdvancedThemeSettings from './theme/AdvancedThemeSettings'; import AdvancedThemeSettings from './theme/AdvancedThemeSettings';
import InvalidThemeModal from './theme/InvalidThemeModal'; import InvalidThemeModal, {FatalErrors} from './theme/InvalidThemeModal';
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react'; import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
import OfficialThemes from './theme/OfficialThemes'; import OfficialThemes from './theme/OfficialThemes';
import React, {useEffect, useRef, useState} from 'react'; import React, {useEffect, useState} from 'react';
import ThemeInstalledModal from './theme/ThemeInstalledModal'; import ThemeInstalledModal from './theme/ThemeInstalledModal';
import ThemePreview from './theme/ThemePreview'; import ThemePreview from './theme/ThemePreview';
import {Breadcrumbs, Button, ConfirmationModal, FileUpload, LimitModal, Modal, PageHeader, TabView, showToast} from '@tryghost/admin-x-design-system'; import {Breadcrumbs, Button, ConfirmationModal, FileUpload, LimitModal, Modal, PageHeader, TabView, showToast} from '@tryghost/admin-x-design-system';
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter'; import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
import {InstalledTheme, Theme, ThemesInstallResponseType, isDefaultOrLegacyTheme, useActivateTheme, useBrowseThemes, useInstallTheme, useUploadTheme} from '@tryghost/admin-x-framework/api/themes'; import {InstalledTheme, Theme, ThemesInstallResponseType, isDefaultOrLegacyTheme, useActivateTheme, useBrowseThemes, useInstallTheme, useUploadTheme} from '@tryghost/admin-x-framework/api/themes';
import {JSONError} from '@tryghost/admin-x-framework/errors';
import {OfficialTheme} from '../../providers/SettingsAppProvider'; import {OfficialTheme} from '../../providers/SettingsAppProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing'; import {useRouting} from '@tryghost/admin-x-framework/routing';
@ -61,14 +62,6 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
const [isUploading, setUploading] = useState(false); const [isUploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleRetry = () => {
if (fileInputRef?.current) {
fileInputRef.current.click();
}
};
useEffect(() => { useEffect(() => {
if (limiter) { if (limiter) {
// Sending a bad string to make sure it fails (empty string isn't valid) // Sending a bad string to make sure it fails (empty string isn't valid)
@ -132,7 +125,7 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
onActivate?: () => void onActivate?: () => void
}) => { }) => {
let data: ThemesInstallResponseType | undefined; let data: ThemesInstallResponseType | undefined;
let fatalErrors = null; let fatalErrors: FatalErrors | null = null;
try { try {
setUploading(true); setUploading(true);
@ -140,9 +133,11 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
setUploading(false); setUploading(false);
} catch (e) { } catch (e) {
setUploading(false); setUploading(false);
const errorsJson = await handleError(e) as {errors?: []};
if (errorsJson?.errors) { if (e instanceof JSONError && e.response?.status === 422 && e.data?.errors) {
fatalErrors = errorsJson.errors; fatalErrors = (e.data.errors as any) as FatalErrors;
} else {
handleError(e);
} }
} }
@ -155,7 +150,7 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
fatalErrors, fatalErrors,
onRetry: async (modal) => { onRetry: async (modal) => {
modal?.remove(); modal?.remove();
handleRetry(); handleUpload();
} }
}); });
} }

View File

@ -9,7 +9,7 @@ type FatalError = {
}; };
}; };
type FatalErrors = FatalError[]; export type FatalErrors = FatalError[];
export const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => { export const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => {
const [isExpanded, setExpanded] = useState(false); const [isExpanded, setExpanded] = useState(false);
@ -63,8 +63,7 @@ const InvalidThemeModal: React.FC<{
if (fatalErrors) { if (fatalErrors) {
warningPrompt = <div className="mt-10"> warningPrompt = <div className="mt-10">
<List title="Errors"> <List title="Errors">
{fatalErrors?.map((error: any) => error?.details?.errors?.map((err: any) => <ThemeProblemView problem={err} /> {fatalErrors?.map(error => error?.details?.errors?.map(err => <ThemeProblemView problem={err} />))}
))}
</List> </List>
</div>; </div>;
} }