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',