Improve technical errors (#20046)
ref DES-229 Some of the error messages in Ghost and more specifically in Settings were very technical, e.g. `ValidationError: Validation (isEmpty) failed for locale` This PR deals with some of the occurances for a more human error communication.
This commit is contained in:
parent
571171bad5
commit
9f19e334c1
@ -80,15 +80,6 @@ export const ResizeDisabled: Story = {
|
||||
}
|
||||
};
|
||||
|
||||
export const MaxLength: Story = {
|
||||
args: {
|
||||
title: 'Description',
|
||||
placeholder: 'Try to enter more than 80 characters, I dare you...',
|
||||
value: 'This is a nice text area that only accepts up to 80 characters. Try to add more:',
|
||||
maxLength: 80
|
||||
}
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
title: 'Description',
|
||||
|
@ -97,7 +97,6 @@ const TextArea: React.FC<TextAreaProps> = ({
|
||||
</textarea>
|
||||
{title && <Heading className={'order-1'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{hint && <Hint className='order-3' color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
{maxLength && <Hint>Max length is {maxLength}</Hint>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -63,6 +63,7 @@ const AddIntegrationModal: React.FC<RoutingModalProps> = () => {
|
||||
autoFocus={true}
|
||||
error={!!errors.name}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
placeholder='Custom integration'
|
||||
title='Name'
|
||||
value={name}
|
||||
|
@ -2,6 +2,7 @@ import APIKeys from './APIKeys';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import WebhooksTable from './WebhooksTable';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {APIKey, useRefreshAPIKey} from '@tryghost/admin-x-framework/api/apiKeys';
|
||||
import {ConfirmationModal, Form, ImageUpload, Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {Integration, useBrowseIntegrations, useEditIntegration} from '@tryghost/admin-x-framework/api/integrations';
|
||||
@ -112,7 +113,11 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateForm(state => ({...state, icon_image: imageUrl}));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -124,12 +129,13 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
title='Title'
|
||||
value={formState.name}
|
||||
onChange={e => updateForm(state => ({...state, name: e.target.value}))}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextField title='Description' value={formState.description || ''} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
|
||||
<TextField maxLength={2000} title='Description' value={formState.description || ''} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
|
||||
<APIKeys keys={[
|
||||
{
|
||||
label: 'Content API key',
|
||||
|
@ -78,6 +78,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
placeholder='Custom webhook'
|
||||
title='Name'
|
||||
value={formState.name}
|
||||
@ -101,6 +102,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
||||
<TextField
|
||||
error={Boolean(errors.target_url)}
|
||||
hint={errors.target_url}
|
||||
maxLength={2000}
|
||||
placeholder='https://example.com'
|
||||
title='Target URL'
|
||||
type='url'
|
||||
@ -109,6 +111,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
||||
onKeyDown={() => clearError('target_url')}
|
||||
/>
|
||||
<TextField
|
||||
maxLength={191}
|
||||
placeholder='https://example.com'
|
||||
title='Secret'
|
||||
value={formState.secret || undefined}
|
||||
|
@ -71,10 +71,10 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
onOk: confirmModal => confirmModal?.remove()
|
||||
});
|
||||
} catch (e) {
|
||||
let prompt = 'There was an error verifying your email address. Please try again.';
|
||||
let prompt = 'There was an error verifying your email address. Try again later.';
|
||||
|
||||
if (e instanceof APIError && e.message === 'Token expired') {
|
||||
prompt = 'The verification link has expired. Please try again.';
|
||||
prompt = 'Verification link has expired.';
|
||||
}
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Error verifying email address',
|
||||
|
@ -97,6 +97,7 @@ const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
|
||||
autoFocus={true}
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
placeholder='Weekly roundup'
|
||||
title='Name'
|
||||
value={formState.name}
|
||||
@ -104,6 +105,7 @@ const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextArea
|
||||
maxLength={2000}
|
||||
title='Description'
|
||||
value={formState.description}
|
||||
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
|
||||
|
@ -76,6 +76,7 @@ const ReplyToEmailField: React.FC<{
|
||||
<TextField
|
||||
error={Boolean(errors.sender_reply_to)}
|
||||
hint={errors.sender_reply_to}
|
||||
maxLength={191}
|
||||
placeholder={newsletterAddress || ''}
|
||||
title="Reply-to email"
|
||||
value={senderReplyTo}
|
||||
@ -201,6 +202,7 @@ const Sidebar: React.FC<{
|
||||
<TextField
|
||||
error={Boolean(errors.sender_email)}
|
||||
hint={errors.sender_email}
|
||||
maxLength={191}
|
||||
placeholder={defaultEmailAddress}
|
||||
title="Sender email address"
|
||||
value={newsletter.sender_email || ''}
|
||||
@ -226,16 +228,17 @@ const Sidebar: React.FC<{
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
placeholder="Weekly Roundup"
|
||||
title="Name"
|
||||
value={newsletter.name || ''}
|
||||
onChange={e => updateNewsletter({name: e.target.value})}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextArea rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
|
||||
<TextArea maxLength={2000} rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
|
||||
</Form>
|
||||
<Form className='mt-6' gap='sm' margins='lg' title='Email info'>
|
||||
<TextField placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||
<TextField maxLength={191} placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||
{renderSenderEmailField()}
|
||||
<ReplyToEmailField clearError={clearError} errors={errors} newsletter={newsletter} updateNewsletter={updateNewsletter} validate={validate} />
|
||||
</Form>
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import TopLevelGroup from '../../TopLevelGroup';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {FacebookLogo, ImageUpload, SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||
@ -43,7 +44,11 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('og_image', imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -95,12 +100,14 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<div className="flex flex-col gap-x-6 gap-y-7 px-4 pb-7">
|
||||
<TextField
|
||||
inputRef={focusRef}
|
||||
maxLength={300}
|
||||
placeholder={siteTitle}
|
||||
title="Facebook title"
|
||||
value={facebookTitle}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<TextField
|
||||
maxLength={300}
|
||||
placeholder={siteDescription}
|
||||
title="Facebook description"
|
||||
value={facebookDescription}
|
||||
|
@ -21,7 +21,7 @@ const LockSite: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
onValidate: () => {
|
||||
if (passwordEnabled && !password) {
|
||||
return {
|
||||
password: 'Password must be supplied'
|
||||
password: 'Enter a password'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,7 @@ const Metadata: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<TextField
|
||||
hint="Recommended: 70 characters"
|
||||
inputRef={focusRef}
|
||||
maxLength={300}
|
||||
placeholder={siteTitle}
|
||||
title="Meta title"
|
||||
value={metaTitle}
|
||||
@ -90,6 +91,7 @@ const Metadata: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
/>
|
||||
<TextField
|
||||
hint="Recommended: 156 characters"
|
||||
maxLength={500}
|
||||
placeholder={siteDescription}
|
||||
title="Meta description"
|
||||
value={metaDescription}
|
||||
|
@ -49,6 +49,7 @@ const TitleAndDescription: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<TextField
|
||||
hint="The name of your site"
|
||||
inputRef={focusRef}
|
||||
maxLength={150}
|
||||
placeholder="Site title"
|
||||
title="Site title"
|
||||
value={title}
|
||||
@ -56,6 +57,7 @@ const TitleAndDescription: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
/>
|
||||
<TextField
|
||||
hint="A short description, used in your theme, meta data and search results"
|
||||
maxLength={200}
|
||||
placeholder="Site description"
|
||||
title="Site description"
|
||||
value={description}
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import TopLevelGroup from '../../TopLevelGroup';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {ImageUpload, SettingGroupContent, TextField, XLogo, withErrorBoundary} from '@tryghost/admin-x-design-system';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||
@ -41,7 +42,11 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
updateSetting('twitter_image', imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -91,12 +96,14 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<div className="flex flex-col gap-x-6 gap-y-7 px-4 pb-7">
|
||||
<TextField
|
||||
inputRef={focusRef}
|
||||
maxLength={300}
|
||||
placeholder={siteTitle}
|
||||
title="X title"
|
||||
value={twitterTitle}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<TextField
|
||||
maxLength={300}
|
||||
placeholder={siteDescription}
|
||||
title="X description"
|
||||
value={twitterDescription}
|
||||
|
@ -9,6 +9,7 @@ import clsx from 'clsx';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import validator from 'validator';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {ConfirmationModal, Heading, Icon, ImageUpload, LimitModal, Menu, MenuItem, Modal, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||
@ -267,7 +268,11 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -374,7 +379,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
||||
<div className='flex w-full max-w-[620px] flex-col gap-5 p-8 md:max-w-[auto] md:flex-row md:items-center'>
|
||||
<div>
|
||||
<ImageUpload
|
||||
deleteButtonClassName='md:invisible absolute pr-3 -right-2 -top-2 flex h-8 w-16 cursor-pointer items-center justify-end rounded-full bg-[rgba(0,0,0,0.75)] text-white group-hover:!visible'
|
||||
deleteButtonClassName='md:invisible absolute pr-3 -right-2 -top-2 flex h-8 w-10 cursor-pointer items-center justify-end rounded-full bg-[rgba(0,0,0,0.75)] text-white group-hover:!visible'
|
||||
deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />}
|
||||
editButtonClassName='md:invisible absolute right-[22px] -top-2 flex h-8 w-8 cursor-pointer items-center justify-center text-white group-hover:!visible z-20'
|
||||
fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]'
|
||||
|
@ -13,6 +13,7 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, validateField, clearErr
|
||||
<TextField
|
||||
error={!!errors?.name}
|
||||
hint={errors?.name || 'Use real name so people can recognize you'}
|
||||
maxLength={191}
|
||||
title="Full name"
|
||||
value={user.name}
|
||||
onBlur={(e) => {
|
||||
@ -26,6 +27,7 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, validateField, clearErr
|
||||
<TextField
|
||||
error={!!errors?.email}
|
||||
hint={errors?.email || 'Used for notifications'}
|
||||
maxLength={191}
|
||||
title="Email"
|
||||
value={user.email}
|
||||
onBlur={(e) => {
|
||||
@ -38,6 +40,7 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, validateField, clearErr
|
||||
/>
|
||||
<TextField
|
||||
hint="https://example.com/author"
|
||||
maxLength={191}
|
||||
title="Slug"
|
||||
value={user.slug}
|
||||
onChange={(e) => {
|
||||
|
@ -13,6 +13,7 @@ export const DetailsInputs: React.FC<UserDetailProps> = ({errors, clearError, va
|
||||
<TextField
|
||||
error={!!errors?.location}
|
||||
hint={errors?.location || 'Where in the world do you live?'}
|
||||
maxLength={65535}
|
||||
title="Location"
|
||||
value={user.location || ''}
|
||||
onBlur={(e) => {
|
||||
@ -25,6 +26,7 @@ export const DetailsInputs: React.FC<UserDetailProps> = ({errors, clearError, va
|
||||
<TextField
|
||||
error={!!errors?.url}
|
||||
hint={errors?.url || 'Have a website or blog other than this one? Link it!'}
|
||||
maxLength={2000}
|
||||
title="Website"
|
||||
value={user.website || ''}
|
||||
onBlur={(e) => {
|
||||
@ -37,6 +39,7 @@ export const DetailsInputs: React.FC<UserDetailProps> = ({errors, clearError, va
|
||||
<TextField
|
||||
error={!!errors?.facebook}
|
||||
hint={errors?.facebook || 'URL of your personal Facebook Profile'}
|
||||
maxLength={2000}
|
||||
title="Facebook profile"
|
||||
value={facebookUrl}
|
||||
onBlur={(e) => {
|
||||
@ -53,6 +56,7 @@ export const DetailsInputs: React.FC<UserDetailProps> = ({errors, clearError, va
|
||||
<TextField
|
||||
error={!!errors?.twitter}
|
||||
hint={errors?.twitter || 'URL of your X profile'}
|
||||
maxLength={2000}
|
||||
title="X (formerly Twitter) profile"
|
||||
value={twitterUrl}
|
||||
onBlur={(e) => {
|
||||
@ -69,6 +73,7 @@ export const DetailsInputs: React.FC<UserDetailProps> = ({errors, clearError, va
|
||||
<TextArea
|
||||
error={!!errors?.bio}
|
||||
hint={errors?.bio || <>Recommended: 200 characters. You‘ve used <span className='font-bold'>{user.bio?.length || 0}</span></>}
|
||||
maxLength={65535}
|
||||
title="Bio"
|
||||
value={user.bio || ''}
|
||||
onBlur={(e) => {
|
||||
|
@ -200,6 +200,7 @@ const AddRecommendationModal: React.FC<RoutingModalProps & AddRecommendationModa
|
||||
autoFocus={true}
|
||||
error={Boolean(errors.url)}
|
||||
hint={errors.url || <>Need inspiration? <a className='text-green' href="https://www.ghost.org/explore" rel="noopener noreferrer" target='_blank'>Explore thousands of sites</a> to recommend</>}
|
||||
maxLength={2000}
|
||||
placeholder='https://www.example.com'
|
||||
title='URL'
|
||||
value={formState.url}
|
||||
|
@ -100,6 +100,7 @@ const RecommendationDescriptionForm: React.FC<Props<EditOrAddRecommendation | Re
|
||||
autoFocus={true}
|
||||
error={Boolean(errors.title)}
|
||||
hint={errors.title}
|
||||
maxLength={2000}
|
||||
title="Title"
|
||||
value={formState.title ?? ''}
|
||||
onChange={(e) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {Form, Heading, Icon, ImageUpload, Select, TextField, Toggle} from '@tryghost/admin-x-design-system';
|
||||
import {ReactComponent as PortalIcon1} from '../../../../assets/icons/portal-icon-1.svg';
|
||||
import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
|
||||
@ -53,7 +54,11 @@ const LookAndFeel: React.FC<{
|
||||
updateSetting('portal_button_icon', imageUrl);
|
||||
setUploadedIcon(imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -95,7 +95,7 @@ const PortalModal: React.FC = () => {
|
||||
let prompt = 'There was an error verifying your email address. Please try again.';
|
||||
|
||||
if (e?.message === 'Token expired') {
|
||||
prompt = 'The verification link has expired. Please try again.';
|
||||
prompt = 'Verification link has expired.';
|
||||
}
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Error verifying support address',
|
||||
|
@ -32,7 +32,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];
|
||||
|
||||
const validators: {[key in keyof Tier]?: () => string | undefined} = {
|
||||
name: () => (formState.name ? undefined : 'You must specify a name'),
|
||||
name: () => (formState.name ? undefined : 'Enter a name for the tier'),
|
||||
monthly_price: () => (formState.type !== 'free' ? validateCurrencyAmount(formState.monthly_price || 0, formState.currency, {allowZero: false}) : undefined),
|
||||
yearly_price: () => (formState.type !== 'free' ? validateCurrencyAmount(formState.yearly_price || 0, formState.currency, {allowZero: false}) : undefined)
|
||||
};
|
||||
@ -213,6 +213,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
autoComplete='off'
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
maxLength={191}
|
||||
placeholder={isFreeTier ? 'Free' : 'Bronze'}
|
||||
title='Name'
|
||||
value={formState.name || ''}
|
||||
@ -223,6 +224,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
<TextField
|
||||
autoComplete='off'
|
||||
autoFocus={isFreeTier}
|
||||
maxLength={191}
|
||||
placeholder={isFreeTier ? `Free preview` : 'Full access to premium content'}
|
||||
title='Description'
|
||||
value={formState.description || ''}
|
||||
@ -297,6 +299,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
<URLTextField
|
||||
baseUrl={siteData?.url}
|
||||
hint={`Redirect to this URL after signup ${isFreeTier ? '' : ' for premium membership'}`}
|
||||
maxLength={2000}
|
||||
placeholder={siteData?.url}
|
||||
title='Welcome page'
|
||||
value={formState.welcome_page_url || null}
|
||||
@ -315,10 +318,11 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
<div className='absolute left-[-32px] top-[7px] flex h-6 w-6 items-center justify-center bg-white group-hover:hidden dark:bg-black'><Icon name='check' size='sm' /></div>
|
||||
<TextField
|
||||
// className='grow border-b border-grey-500 py-2 focus:border-grey-800 group-hover:border-grey-600'
|
||||
maxLength={191}
|
||||
value={item}
|
||||
onChange={e => benefits.updateItem(id, e.target.value)}
|
||||
/>
|
||||
<Button className='absolute right-1 top-1 z-10' icon='trash' iconColorClass='opacity-0 group-hover:opacity-100' size='sm' onClick={() => benefits.removeItem(id)} />
|
||||
<Button className='absolute right-1 top-1 z-10 opacity-0 group-hover:opacity-100' color='grey' icon='trash' size='sm' onClick={() => benefits.removeItem(id)} />
|
||||
</div>}
|
||||
onMove={benefits.moveItem}
|
||||
/>
|
||||
@ -328,6 +332,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
<TextField
|
||||
className='grow'
|
||||
containerClassName='w-100'
|
||||
maxLength={191}
|
||||
placeholder='Expert analysis'
|
||||
title='New benefit'
|
||||
value={benefits.newItem}
|
||||
@ -340,7 +345,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className='absolute right-1 top-1 z-10'
|
||||
className='absolute right-[5px] top-[5px] z-10'
|
||||
color='green'
|
||||
icon='add'
|
||||
iconColorClass='text-white'
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, {useRef, useState} from 'react';
|
||||
import UnsplashSelector from '../../../selectors/UnsplashSelector';
|
||||
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {ColorPickerField, Heading, Hint, ImageUpload, SettingGroupContent, TextField, debounce} from '@tryghost/admin-x-design-system';
|
||||
import {SettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
@ -39,6 +40,7 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||
<TextField
|
||||
key='site-description'
|
||||
hint='Used in your theme, meta data and search results'
|
||||
maxLength={200}
|
||||
title='Site description'
|
||||
value={siteDescription}
|
||||
onChange={(event) => {
|
||||
@ -76,7 +78,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||
try {
|
||||
updateSetting('icon', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -98,7 +104,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||
try {
|
||||
updateSetting('logo', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -136,7 +146,11 @@ const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key:
|
||||
try {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -38,10 +38,6 @@ test.describe('User profile', async () => {
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
await expect(modal).toContainText('Name is required');
|
||||
|
||||
await modal.getByLabel('Full name').fill(new Array(195).join('a'));
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
await expect(modal).toContainText('Name is too long');
|
||||
|
||||
await modal.getByLabel('Email').fill('test');
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
await expect(modal).toContainText('Please enter a valid email address');
|
||||
|
@ -21,7 +21,7 @@ test.describe('Tier settings', async () => {
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast-error')).toHaveText(/Can't save tier/);
|
||||
await expect(modal).toHaveText(/You must specify a name/);
|
||||
await expect(modal).toHaveText(/Enter a name for the tier/);
|
||||
await expect(modal).toHaveText(/Amount must be at least \$1/);
|
||||
|
||||
await modal.getByLabel('Name').fill('Plus tier');
|
||||
@ -107,7 +107,7 @@ test.describe('Tier settings', async () => {
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(page.getByTestId('toast-error')).toHaveText(/Can't save tier/);
|
||||
await expect(modal).toHaveText(/You must specify a name/);
|
||||
await expect(modal).toHaveText(/Enter a name for the tier/);
|
||||
|
||||
// Valid values
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user