mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): input error message (#1050)
This commit is contained in:
parent
8efea2eddc
commit
458602fd64
11 changed files with 147 additions and 31 deletions
|
@ -72,3 +72,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-error);
|
||||
margin-top: _.unit(1);
|
||||
}
|
||||
|
|
|
@ -5,27 +5,31 @@ import * as styles from './index.module.scss';
|
|||
|
||||
type Props = HTMLProps<HTMLInputElement> & {
|
||||
hasError?: boolean;
|
||||
errorMessage?: string;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
const TextInput = (
|
||||
{ hasError = false, icon, disabled, className, readOnly, ...rest }: Props,
|
||||
{ hasError = false, errorMessage, icon, disabled, className, readOnly, ...rest }: Props,
|
||||
reference: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.container,
|
||||
hasError && styles.error,
|
||||
icon && styles.withIcon,
|
||||
disabled && styles.disabled,
|
||||
readOnly && styles.readOnly,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
<input type="text" {...rest} ref={reference} disabled={disabled} readOnly={readOnly} />
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.container,
|
||||
hasError && styles.error,
|
||||
icon && styles.withIcon,
|
||||
disabled && styles.disabled,
|
||||
readOnly && styles.readOnly,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{icon && <span className={styles.icon}>{icon}</span>}
|
||||
<input type="text" {...rest} ref={reference} disabled={disabled} readOnly={readOnly} />
|
||||
</div>
|
||||
{hasError && errorMessage && <div className={styles.errorMessage}>{errorMessage}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ const ApiResourceDetails = () => {
|
|||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FormData>({
|
||||
defaultValues: data,
|
||||
});
|
||||
|
@ -152,7 +152,10 @@ const ApiResourceDetails = () => {
|
|||
title="admin_console.api_resources.api_name"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
<TextInput
|
||||
{...register('name', { required: true })}
|
||||
hasError={Boolean(errors.name)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
isRequired
|
||||
|
@ -161,6 +164,7 @@ const ApiResourceDetails = () => {
|
|||
>
|
||||
<TextInput
|
||||
{...register('accessTokenTtl', { required: true, valueAsNumber: true })}
|
||||
hasError={Boolean(errors.accessTokenTtl)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,11 @@ type Props = {
|
|||
};
|
||||
|
||||
const Settings = ({ oidcConfig }: Props) => {
|
||||
const { control, register } = useFormContext<Application>();
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<Application>();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const uriPatternRules: MultiTextInputRule = {
|
||||
|
@ -35,7 +39,7 @@ const Settings = ({ oidcConfig }: Props) => {
|
|||
title="admin_console.application_details.application_name"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
<TextInput {...register('name', { required: true })} hasError={Boolean(errors.name)} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.application_details.description" className={styles.textField}>
|
||||
<TextInput {...register('description')} />
|
||||
|
|
|
@ -121,7 +121,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
)}
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.applications.application_name">
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
<TextInput {...register('name', { required: true })} hasError={Boolean(errors.name)} />
|
||||
</FormField>
|
||||
<FormField title="admin_console.applications.application_description">
|
||||
<TextInput {...register('description')} />
|
||||
|
|
|
@ -8,13 +8,19 @@ import FormField from '@/components/FormField';
|
|||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import Switch from '@/components/Switch';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import { uriValidator } from '@/utilities/validator';
|
||||
|
||||
import { SignInExperienceForm } from '../types';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const BrandingForm = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { watch, register, control } = useFormContext<SignInExperienceForm>();
|
||||
const {
|
||||
watch,
|
||||
register,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const isDarkModeEnabled = watch('branding.isDarkModeEnabled');
|
||||
const style = watch('branding.style');
|
||||
|
@ -66,15 +72,47 @@ const BrandingForm = () => {
|
|||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.sign_in_exp.branding.logo_image_url">
|
||||
<TextInput {...register('branding.logoUrl', { required: true })} />
|
||||
<TextInput
|
||||
{...register('branding.logoUrl', {
|
||||
required: true,
|
||||
validate: (value) => {
|
||||
if (uriValidator({ verifyBlank: false })(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return t('errors.invalid_uri_format');
|
||||
},
|
||||
})}
|
||||
hasError={Boolean(errors.branding?.logoUrl)}
|
||||
errorMessage={errors.branding?.logoUrl?.message}
|
||||
/>
|
||||
</FormField>
|
||||
{isDarkModeEnabled && (
|
||||
<FormField title="admin_console.sign_in_exp.branding.dark_logo_image_url">
|
||||
<TextInput {...register('branding.darkLogoUrl')} />
|
||||
<TextInput
|
||||
{...register('branding.darkLogoUrl', {
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uriValidator({ verifyBlank: false })(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return t('errors.invalid_uri_format');
|
||||
},
|
||||
})}
|
||||
hasError={Boolean(errors.branding?.darkLogoUrl)}
|
||||
errorMessage={errors.branding?.darkLogoUrl?.message}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField isRequired={isSloganRequired} title="admin_console.sign_in_exp.branding.slogan">
|
||||
<TextInput {...register('branding.slogan', { required: isSloganRequired })} />
|
||||
<TextInput
|
||||
{...register('branding.slogan', { required: isSloganRequired })}
|
||||
hasError={Boolean(isSloganRequired && errors.branding?.slogan)}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -11,7 +11,11 @@ import * as styles from './index.module.scss';
|
|||
|
||||
const TermsForm = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { watch, register } = useFormContext<SignInExperienceForm>();
|
||||
const {
|
||||
watch,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
const enabled = watch('termsOfUse.enabled');
|
||||
|
||||
return (
|
||||
|
@ -28,7 +32,10 @@ const TermsForm = () => {
|
|||
title="admin_console.sign_in_exp.terms_of_use.terms_of_use"
|
||||
tooltip="admin_console.sign_in_exp.terms_of_use.terms_of_use_tip"
|
||||
>
|
||||
<TextInput {...register('termsOfUse.contentUrl', { required: enabled })} />
|
||||
<TextInput
|
||||
{...register('termsOfUse.contentUrl', { required: enabled })}
|
||||
hasError={Boolean(enabled && errors.termsOfUse)}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -28,6 +28,7 @@ import Reset from '@/icons/Reset';
|
|||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { safeParseJson } from '@/utilities/json';
|
||||
import { uriValidator } from '@/utilities/validator';
|
||||
|
||||
import CreateSuccess from './components/CreateSuccess';
|
||||
import DeleteForm from './components/DeleteForm';
|
||||
|
@ -60,7 +61,7 @@ const UserDetails = () => {
|
|||
register,
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
formState: { isSubmitting, errors },
|
||||
getValues,
|
||||
} = useForm<FormData>();
|
||||
|
||||
|
@ -221,7 +222,23 @@ const UserDetails = () => {
|
|||
title="admin_console.user_details.field_avatar"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('avatar')} />
|
||||
<TextInput
|
||||
{...register('avatar', {
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uriValidator({ verifyBlank: true })(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return t('errors.invalid_uri_format');
|
||||
},
|
||||
})}
|
||||
hasError={Boolean(errors.avatar)}
|
||||
errorMessage={errors.avatar?.message}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
title="admin_console.user_details.field_roles"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import { passwordRegEx, usernameRegEx } from '@logto/shared';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
|
@ -22,8 +24,10 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FormData>();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
|
@ -53,13 +57,40 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
>
|
||||
<form>
|
||||
<FormField isRequired title="admin_console.users.create_form_username">
|
||||
<TextInput autoFocus {...register('username', { required: true })} />
|
||||
<TextInput
|
||||
autoFocus
|
||||
{...register('username', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: usernameRegEx,
|
||||
message: t('errors.username_pattern_error'),
|
||||
},
|
||||
})}
|
||||
hasError={Boolean(errors.username)}
|
||||
errorMessage={errors.username?.message}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.users.create_form_name">
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
<TextInput
|
||||
{...register('name', {
|
||||
required: true,
|
||||
})}
|
||||
hasError={Boolean(errors.name)}
|
||||
errorMessage={errors.name?.message}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.users.create_form_password">
|
||||
<TextInput {...register('password', { required: true })} />
|
||||
<TextInput
|
||||
{...register('password', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: passwordRegEx,
|
||||
message: t('errors.password_pattern_error'),
|
||||
},
|
||||
})}
|
||||
hasError={Boolean(errors.password)}
|
||||
errorMessage={errors.password?.message}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
</ModalLayout>
|
||||
|
|
|
@ -121,6 +121,9 @@ const translation = {
|
|||
required_field_missing: 'Please enter {{field}}',
|
||||
required_field_missing_plural: 'You have to enter at least one {{field}}',
|
||||
more_details: 'More details',
|
||||
username_pattern_error:
|
||||
'Username should only contain letters, numbers, or underscore and should not start with a number.',
|
||||
password_pattern_error: 'Password requires a minimum of 6 characters.',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: 'Overview',
|
||||
|
|
|
@ -121,6 +121,8 @@ const translation = {
|
|||
required_field_missing: '请输入{{field}}',
|
||||
required_field_missing_plural: '{{field}}不能全部为空',
|
||||
more_details: '查看详情',
|
||||
username_pattern_error: '用户名只能包含英文字母、数字或下划线,且不以数字开头。',
|
||||
password_pattern_error: '密码应不少于 6 位。',
|
||||
},
|
||||
tab_sections: {
|
||||
overview: '概览',
|
||||
|
|
Loading…
Reference in a new issue