mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
AdminX user settings related cleanup (#17520)
refs. https://github.com/TryGhost/Product/issues/3349 - added validation for user name - added onBlur validation for fields - changed textfield label color on focus
This commit is contained in:
parent
e45b947d61
commit
0a0fcbc551
5 changed files with 73 additions and 34 deletions
|
@ -46,7 +46,7 @@ function App({ghostVersion, officialThemes}: AppProps) {
|
|||
</div>
|
||||
|
||||
<div className="relative flex-auto pt-[3vmin] md:ml-[300px] md:pt-[85px]">
|
||||
<div className='fixed inset-x-0 top-0 z-[5] h-[130px] bg-gradient-to-t from-transparent to-white to-60%'></div>
|
||||
<div className='pointer-events-none fixed inset-x-0 top-0 z-[5] h-[130px] bg-gradient-to-t from-transparent to-white to-60%'></div>
|
||||
<Settings />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,6 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
type = 'text',
|
||||
inputRef,
|
||||
title,
|
||||
//titleColor = 'grey',
|
||||
hideTitle,
|
||||
value,
|
||||
error,
|
||||
|
@ -50,12 +49,12 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
const id = useId();
|
||||
|
||||
const textFieldClasses = !unstyled && clsx(
|
||||
'h-10 w-full border-b py-2',
|
||||
'peer order-2 h-10 w-full border-b py-2',
|
||||
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
|
||||
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`,
|
||||
(title && !hideTitle && !clearBg) && `mt-2`,
|
||||
(disabled ? 'text-grey-700' : ''),
|
||||
rightPlaceholder && 'peer w-0 grow',
|
||||
rightPlaceholder && 'w-0 grow',
|
||||
className
|
||||
);
|
||||
|
||||
|
@ -91,17 +90,11 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
}
|
||||
|
||||
if (title || hint) {
|
||||
// let titleGrey = false;
|
||||
// if (titleColor === 'auto') {
|
||||
// titleGrey = value ? true : false;
|
||||
// } else {
|
||||
// titleGrey = titleColor === 'grey' ? true : false;
|
||||
// }
|
||||
return (
|
||||
<div className={`flex flex-col ${containerClassName}`}>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={true} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{field}
|
||||
{hint && <Hint className={hintClassName} color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : 'order-1 !text-grey-700 peer-focus:!text-black'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{hint && <Hint className={'order-3' + hintClassName} color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {FileService, ServicesContext} from '../../providers/ServiceProvider';
|
|||
import {User} from '../../../types/api';
|
||||
import {isAdminUser, isOwnerUser} from '../../../utils/helpers';
|
||||
import {showToast} from '../../../admin-x-ds/global/Toast';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface CustomHeadingProps {
|
||||
children?: React.ReactNode;
|
||||
|
@ -28,9 +29,15 @@ interface UserDetailProps {
|
|||
user: User;
|
||||
setUserData?: (user: User) => void;
|
||||
errors?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
email?: string;
|
||||
};
|
||||
validators?: {
|
||||
name: (name: string) => boolean,
|
||||
email: (email: string) => boolean,
|
||||
url: (url: string) => boolean
|
||||
}
|
||||
}
|
||||
|
||||
const CustomHeader: React.FC<CustomHeadingProps> = ({children}) => {
|
||||
|
@ -89,13 +96,18 @@ const RoleSelector: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
|||
/>
|
||||
);
|
||||
};
|
||||
const BasicInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
|
||||
const BasicInputs: React.FC<UserDetailProps> = ({errors, validators, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroupContent>
|
||||
<TextField
|
||||
hint="Use real name so people can recognize you"
|
||||
error={!!errors?.name}
|
||||
hint={errors?.name || "Use real name so people can recognize you"}
|
||||
title="Full name"
|
||||
value={user.name}
|
||||
onBlur={(e) => {
|
||||
validators?.name(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setUserData?.({...user, name: e.target.value});
|
||||
}}
|
||||
|
@ -105,6 +117,9 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) =>
|
|||
hint={errors?.email || ''}
|
||||
title="Email"
|
||||
value={user.email}
|
||||
onBlur={(e) => {
|
||||
validators?.email(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setUserData?.({...user, email: e.target.value});
|
||||
}}
|
||||
|
@ -114,19 +129,19 @@ const BasicInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) =>
|
|||
);
|
||||
};
|
||||
|
||||
const Basic: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
const Basic: React.FC<UserDetailProps> = ({errors, validators, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroup
|
||||
border={false}
|
||||
customHeader={<CustomHeader>Basic info</CustomHeader>}
|
||||
title='Basic'
|
||||
>
|
||||
<BasicInputs errors={errors} setUserData={setUserData} user={user} />
|
||||
<BasicInputs errors={errors} setUserData={setUserData} user={user} validators={validators} />
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailsInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
const DetailsInputs: React.FC<UserDetailProps> = ({errors, validators, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroupContent>
|
||||
<TextField
|
||||
|
@ -149,6 +164,9 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) =
|
|||
hint={errors?.url || ''}
|
||||
title="Website"
|
||||
value={user.website}
|
||||
onBlur={(e) => {
|
||||
validators?.url(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setUserData?.({...user, website: e.target.value});
|
||||
}}
|
||||
|
@ -179,14 +197,14 @@ const DetailsInputs: React.FC<UserDetailProps> = ({errors, user, setUserData}) =
|
|||
);
|
||||
};
|
||||
|
||||
const Details: React.FC<UserDetailProps> = ({errors, user, setUserData}) => {
|
||||
const Details: React.FC<UserDetailProps> = ({errors, validators, user, setUserData}) => {
|
||||
return (
|
||||
<SettingGroup
|
||||
border={false}
|
||||
customHeader={<CustomHeader>Details</CustomHeader>}
|
||||
title='Details'
|
||||
>
|
||||
<DetailsInputs errors={errors} setUserData={setUserData} user={user} />
|
||||
<DetailsInputs errors={errors} setUserData={setUserData} user={user} validators={validators} />
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -406,6 +424,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
const [userData, setUserData] = useState(user);
|
||||
const [saveState, setSaveState] = useState('');
|
||||
const [errors, setErrors] = useState<{
|
||||
name?: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
}>({});
|
||||
|
@ -569,6 +588,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
]);
|
||||
|
||||
let okLabel = saveState === 'saved' ? 'Saved' : 'Save & close';
|
||||
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Saving...';
|
||||
} else if (saveState === 'saved') {
|
||||
|
@ -582,6 +602,29 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
|
||||
const suspendedText = userData.status === 'inactive' ? ' (Suspended)' : '';
|
||||
|
||||
const validators = {
|
||||
name: (name: string) => {
|
||||
setErrors?.((_errors) => {
|
||||
return {..._errors, name: name ? '' : 'Please enter a name'};
|
||||
});
|
||||
return !!name;
|
||||
},
|
||||
email: (email: string) => {
|
||||
const valid = validator.isEmail(email);
|
||||
setErrors?.((_errors) => {
|
||||
return {..._errors, email: valid ? '' : 'Please enter a valid email address'};
|
||||
});
|
||||
return valid;
|
||||
},
|
||||
url: (url: string) => {
|
||||
const valid = !url || validator.isURL(url);
|
||||
setErrors?.((_errors) => {
|
||||
return {..._errors, url: valid ? '' : 'Please enter a valid URL'};
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
okLabel={okLabel}
|
||||
|
@ -590,21 +633,22 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
testId='user-detail-modal'
|
||||
onOk={async () => {
|
||||
setSaveState('saving');
|
||||
if (!validator.isEmail(userData.email)) {
|
||||
setErrors?.((_errors) => {
|
||||
return {..._errors, email: 'Please enter a valid email address'};
|
||||
});
|
||||
setSaveState('');
|
||||
return;
|
||||
}
|
||||
if (!validator.isURL(userData.url)) {
|
||||
setErrors?.((_errors) => {
|
||||
return {..._errors, url: 'Please enter a valid URL'};
|
||||
let error = false;
|
||||
if (!validators.name(userData.name) || !validators.email(userData.email) || !validators.url(userData.website)) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: "Can't save user! One or more fields have errors, please doublecheck you filled all mandatory fields"
|
||||
});
|
||||
setSaveState('');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.dismiss();
|
||||
|
||||
await updateUser?.(userData);
|
||||
setSaveState('saved');
|
||||
}}
|
||||
|
@ -658,8 +702,8 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className='mt-10 grid grid-cols-2 gap-x-12 gap-y-20'>
|
||||
<Basic errors={errors} setUserData={setUserData} user={userData} />
|
||||
<Details errors={errors} setUserData={setUserData} user={userData} />
|
||||
<Basic errors={errors} setUserData={setUserData} user={userData} validators={validators} />
|
||||
<Details errors={errors} setUserData={setUserData} user={userData} validators={validators} />
|
||||
<EmailNotifications setUserData={setUserData} user={userData} />
|
||||
<Password user={userData} />
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,7 @@ import {Tier} from '../../../../types/api';
|
|||
import {currencies, currencyFromDecimal, currencyGroups, currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
import {getSettingValues} from '../../../../utils/helpers';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {useTiers} from '../../../providers/ServiceProvider';
|
||||
|
||||
interface TierDetailModalProps {
|
||||
|
@ -121,7 +122,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
if (Object.values(validators).filter(validator => validator()).length) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'One or more fields have errors'
|
||||
message: "Can't save tier! One or more fields have errors, please doublecheck you filled all mandatory fields"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -129,6 +130,7 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
handleSave();
|
||||
|
||||
if (saveState !== 'unsaved') {
|
||||
toast.dismiss();
|
||||
modal.remove();
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -32,7 +32,7 @@ test.describe('User profile', async () => {
|
|||
await modal.getByLabel('Email').fill('newadmin@test.com');
|
||||
await modal.getByLabel('Slug').fill('newadmin');
|
||||
await modal.getByLabel('Location').fill('some location');
|
||||
await modal.getByLabel('Website').fill('some site');
|
||||
await modal.getByLabel('Website').fill('https://example.com');
|
||||
await modal.getByLabel('Facebook profile').fill('some fb');
|
||||
await modal.getByLabel('Twitter profile').fill('some tw');
|
||||
await modal.getByLabel('Bio').fill('some bio');
|
||||
|
@ -53,7 +53,7 @@ test.describe('User profile', async () => {
|
|||
name: 'New Admin',
|
||||
slug: 'newadmin',
|
||||
location: 'some location',
|
||||
website: 'some site',
|
||||
website: 'https://example.com',
|
||||
facebook: 'some fb',
|
||||
twitter: 'some tw',
|
||||
bio: 'some bio',
|
||||
|
|
Loading…
Add table
Reference in a new issue