diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index a7c10a5589..6e8ed8b29f 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -46,7 +46,7 @@ function App({ghostVersion, officialThemes}: AppProps) {
-
+
diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx index f9057bad94..4d529bf27c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx @@ -29,7 +29,6 @@ const TextField: React.FC = ({ type = 'text', inputRef, title, - //titleColor = 'grey', hideTitle, value, error, @@ -50,12 +49,12 @@ const TextField: React.FC = ({ 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 = ({ } if (title || hint) { - // let titleGrey = false; - // if (titleColor === 'auto') { - // titleGrey = value ? true : false; - // } else { - // titleGrey = titleColor === 'grey' ? true : false; - // } return (
- {title && {title}} {field} - {hint && {hint}} + {title && {title}} + {hint && {hint}}
); } else { diff --git a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx index a917a0450d..1a4aec4448 100644 --- a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx @@ -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 = ({children}) => { @@ -89,13 +96,18 @@ const RoleSelector: React.FC = ({user, setUserData}) => { /> ); }; -const BasicInputs: React.FC = ({errors, user, setUserData}) => { + +const BasicInputs: React.FC = ({errors, validators, user, setUserData}) => { return ( { + validators?.name(e.target.value); + }} onChange={(e) => { setUserData?.({...user, name: e.target.value}); }} @@ -105,6 +117,9 @@ const BasicInputs: React.FC = ({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 = ({errors, user, setUserData}) => ); }; -const Basic: React.FC = ({errors, user, setUserData}) => { +const Basic: React.FC = ({errors, validators, user, setUserData}) => { return ( Basic info} title='Basic' > - + ); }; -const DetailsInputs: React.FC = ({errors, user, setUserData}) => { +const DetailsInputs: React.FC = ({errors, validators, user, setUserData}) => { return ( = ({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 = ({errors, user, setUserData}) = ); }; -const Details: React.FC = ({errors, user, setUserData}) => { +const Details: React.FC = ({errors, validators, user, setUserData}) => { return ( Details} title='Details' > - + ); }; @@ -406,6 +424,7 @@ const UserDetailModal:React.FC = ({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 = ({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 = ({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 ( = ({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 = ({user, updateUser}) => {
- -
+ +
diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx index 8f8867a602..78a6d5cb24 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx @@ -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 = ({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 = ({tier}) => { handleSave(); if (saveState !== 'unsaved') { + toast.dismiss(); modal.remove(); } }} diff --git a/apps/admin-x-settings/test/e2e/general/users/profile.test.ts b/apps/admin-x-settings/test/e2e/general/users/profile.test.ts index 579187667f..56c8d77e70 100644 --- a/apps/admin-x-settings/test/e2e/general/users/profile.test.ts +++ b/apps/admin-x-settings/test/e2e/general/users/profile.test.ts @@ -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',