0
Fork 0
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:
Xiao Yijun 2022-06-07 11:07:31 +08:00 committed by GitHub
parent 8efea2eddc
commit 458602fd64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 31 deletions

View file

@ -72,3 +72,9 @@
}
}
}
.errorMessage {
font: var(--font-body-medium);
color: var(--color-error);
margin-top: _.unit(1);
}

View file

@ -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>}
</>
);
};

View file

@ -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>

View file

@ -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')} />

View file

@ -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')} />

View file

@ -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>
</>
);

View file

@ -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>
</>
);

View file

@ -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"

View file

@ -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>

View file

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

View file

@ -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: '概览',