feat: add i18n translated form errors (#509)
This commit is contained in:
@@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -24,18 +24,17 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
|||||||
const t = useScopedI18n("user");
|
const t = useScopedI18n("user");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate, isPending } = clientApi.user.register.useMutation();
|
const { mutate, isPending } = clientApi.user.register.useMutation();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.user.registration, {
|
||||||
validate: zodResolver(validation.user.registration),
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
},
|
},
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: FormType) => {
|
const handleSubmit = (
|
||||||
|
values: z.infer<typeof validation.user.registration>,
|
||||||
|
) => {
|
||||||
mutate(
|
mutate(
|
||||||
{
|
{
|
||||||
...values,
|
...values,
|
||||||
@@ -88,5 +87,3 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.registration>;
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { signIn } from "@homarr/auth/client";
|
import { signIn } from "@homarr/auth/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -27,15 +27,16 @@ export const LoginForm = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.user.signIn, {
|
||||||
validate: zodResolver(validation.user.signIn),
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmitAsync = async (values: FormType) => {
|
const handleSubmitAsync = async (
|
||||||
|
values: z.infer<typeof validation.user.signIn>,
|
||||||
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
await signIn("credentials", {
|
await signIn("credentials", {
|
||||||
@@ -92,5 +93,3 @@ export const LoginForm = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormType = z.infer<typeof validation.user.signIn>;
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
backgroundImageRepeats,
|
backgroundImageRepeats,
|
||||||
backgroundImageSizes,
|
backgroundImageSizes,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
|
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
|
||||||
import { SelectWithDescriptionBadge } from "@homarr/ui";
|
import { SelectWithDescriptionBadge } from "@homarr/ui";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { useSavePartialSettingsMutation } from "./_shared";
|
import { useSavePartialSettingsMutation } from "./_shared";
|
||||||
@@ -23,7 +24,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate: savePartialSettings, isPending } =
|
const { mutate: savePartialSettings, isPending } =
|
||||||
useSavePartialSettingsMutation(board);
|
useSavePartialSettingsMutation(board);
|
||||||
const form = useForm({
|
const form = useZodForm(validation.board.savePartialSettings, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
||||||
backgroundImageAttachment: board.backgroundImageAttachment,
|
backgroundImageAttachment: board.backgroundImageAttachment,
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { generateColors } from "../../(content)/_theme";
|
import { generateColors } from "../../(content)/_theme";
|
||||||
@@ -33,7 +34,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
|
|||||||
const progressPercentageLabel = (value: number) => `${value}%`;
|
const progressPercentageLabel = (value: number) => `${value}%`;
|
||||||
|
|
||||||
export const ColorSettingsContent = ({ board }: Props) => {
|
export const ColorSettingsContent = ({ board }: Props) => {
|
||||||
const form = useForm({
|
const form = useZodForm(validation.board.savePartialSettings, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
primaryColor: board.primaryColor,
|
primaryColor: board.primaryColor,
|
||||||
secondaryColor: board.secondaryColor,
|
secondaryColor: board.secondaryColor,
|
||||||
@@ -114,15 +115,16 @@ export const ColorSettingsContent = ({ board }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ColorsPreviewProps {
|
interface ColorsPreviewProps {
|
||||||
previewColor: string;
|
previewColor: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
|
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
const colors = hexRegex.test(previewColor)
|
const colors =
|
||||||
? generateColors(previewColor)
|
previewColor && hexRegex.test(previewColor)
|
||||||
: generateColors("#000000");
|
? generateColors(previewColor)
|
||||||
|
: generateColors("#000000");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap={0} wrap="nowrap">
|
<Group gap={0} wrap="nowrap">
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import {
|
|||||||
} from "@mantine/hooks";
|
} from "@mantine/hooks";
|
||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { useUpdateBoard } from "../../(content)/_client";
|
import { useUpdateBoard } from "../../(content)/_client";
|
||||||
@@ -38,20 +39,30 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
|
|
||||||
const { mutate: savePartialSettings, isPending } =
|
const { mutate: savePartialSettings, isPending } =
|
||||||
useSavePartialSettingsMutation(board);
|
useSavePartialSettingsMutation(board);
|
||||||
const form = useForm({
|
const form = useZodForm(
|
||||||
initialValues: {
|
validation.board.savePartialSettings
|
||||||
pageTitle: board.pageTitle ?? "",
|
.pick({
|
||||||
logoImageUrl: board.logoImageUrl ?? "",
|
pageTitle: true,
|
||||||
metaTitle: board.metaTitle ?? "",
|
logoImageUrl: true,
|
||||||
faviconImageUrl: board.faviconImageUrl ?? "",
|
metaTitle: true,
|
||||||
|
faviconImageUrl: true,
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
{
|
||||||
|
initialValues: {
|
||||||
|
pageTitle: board.pageTitle ?? "",
|
||||||
|
logoImageUrl: board.logoImageUrl ?? "",
|
||||||
|
metaTitle: board.metaTitle ?? "",
|
||||||
|
faviconImageUrl: board.faviconImageUrl ?? "",
|
||||||
|
},
|
||||||
|
onValuesChange({ pageTitle }) {
|
||||||
|
updateBoard((previous) => ({
|
||||||
|
...previous,
|
||||||
|
pageTitle,
|
||||||
|
}));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onValuesChange({ pageTitle }) {
|
);
|
||||||
updateBoard((previous) => ({
|
|
||||||
...previous,
|
|
||||||
pageTitle,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
||||||
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
|
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import { Button, Grid, Group, Input, Slider, Stack } from "@mantine/core";
|
import { Button, Grid, Group, Input, Slider, Stack } from "@mantine/core";
|
||||||
|
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { useSavePartialSettingsMutation } from "./_shared";
|
import { useSavePartialSettingsMutation } from "./_shared";
|
||||||
@@ -15,11 +16,14 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate: savePartialSettings, isPending } =
|
const { mutate: savePartialSettings, isPending } =
|
||||||
useSavePartialSettingsMutation(board);
|
useSavePartialSettingsMutation(board);
|
||||||
const form = useForm({
|
const form = useZodForm(
|
||||||
initialValues: {
|
validation.board.savePartialSettings.pick({ columnCount: true }).required(),
|
||||||
columnCount: board.columnCount,
|
{
|
||||||
|
initialValues: {
|
||||||
|
columnCount: board.columnCount,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -18,10 +18,7 @@ export const InitUserForm = () => {
|
|||||||
const t = useScopedI18n("user");
|
const t = useScopedI18n("user");
|
||||||
const { mutateAsync, error, isPending } =
|
const { mutateAsync, error, isPending } =
|
||||||
clientApi.user.initUser.useMutation();
|
clientApi.user.initUser.useMutation();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.user.init, {
|
||||||
validate: zodResolver(validation.user.init),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { z } from "@homarr/validation";
|
import type { z } from "@homarr/validation";
|
||||||
@@ -25,14 +25,13 @@ export const AppForm = (props: AppFormProps) => {
|
|||||||
props;
|
props;
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useZodForm(validation.app.manage, {
|
||||||
initialValues: initialValues ?? {
|
initialValues: initialValues ?? {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
iconUrl: "",
|
iconUrl: "",
|
||||||
href: "",
|
href: "",
|
||||||
},
|
},
|
||||||
validate: zodResolver(validation.app.manage),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,9 +40,7 @@ export const AppForm = (props: AppFormProps) => {
|
|||||||
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
|
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
|
||||||
<IconPicker
|
<IconPicker
|
||||||
initialValue={initialValues?.iconUrl}
|
initialValue={initialValues?.iconUrl}
|
||||||
onChange={(iconUrl) => {
|
{...form.getInputProps("iconUrl")}
|
||||||
form.setFieldValue("iconUrl", iconUrl);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Textarea {...form.getInputProps("description")} label="Description" />
|
<Textarea {...form.getInputProps("description")} label="Description" />
|
||||||
<TextInput {...form.getInputProps("href")} label="URL" />
|
<TextInput {...form.getInputProps("href")} label="URL" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import { integrationSecretIcons } from "./_integration-secret-icons";
|
import { integrationSecretIcons } from "./_integration-secret-icons";
|
||||||
|
|
||||||
interface IntegrationSecretInputProps {
|
interface IntegrationSecretInputProps {
|
||||||
|
withAsterisk?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
getAllSecretKindOptions,
|
getAllSecretKindOptions,
|
||||||
getDefaultSecretKinds,
|
getDefaultSecretKinds,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useConfirmModal } from "@homarr/modals";
|
import { useConfirmModal } from "@homarr/modals";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
@@ -55,9 +55,8 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.integration.update.omit({ id: true }), {
|
||||||
initialValues: initialFormValues,
|
initialValues: initialFormValues,
|
||||||
validate: zodResolver(validation.integration.update.omit({ id: true })),
|
|
||||||
onValuesChange,
|
onValuesChange,
|
||||||
});
|
});
|
||||||
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
||||||
@@ -103,11 +102,13 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
<TestConnectionNoticeAlert />
|
<TestConnectionNoticeAlert />
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
label={t("integration.field.name.label")}
|
label={t("integration.field.name.label")}
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
label={t("integration.field.url.label")}
|
label={t("integration.field.url.label")}
|
||||||
{...form.getInputProps("url")}
|
{...form.getInputProps("url")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||||
import type { UseFormReturnType } from "@homarr/form";
|
import type { UseFormReturnType } from "@homarr/form";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -60,9 +60,8 @@ export const NewIntegrationForm = ({
|
|||||||
initialFormValue: initialFormValues,
|
initialFormValue: initialFormValues,
|
||||||
});
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.integration.create.omit({ kind: true }), {
|
||||||
initialValues: initialFormValues,
|
initialValues: initialFormValues,
|
||||||
validate: zodResolver(validation.integration.create.omit({ kind: true })),
|
|
||||||
onValuesChange,
|
onValuesChange,
|
||||||
});
|
});
|
||||||
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
||||||
@@ -100,11 +99,13 @@ export const NewIntegrationForm = ({
|
|||||||
<TestConnectionNoticeAlert />
|
<TestConnectionNoticeAlert />
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
label={t("integration.field.name.label")}
|
label={t("integration.field.name.label")}
|
||||||
{...form.getInputProps("name")}
|
{...form.getInputProps("name")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
withAsterisk
|
||||||
label={t("integration.field.url.label")}
|
label={t("integration.field.url.label")}
|
||||||
{...form.getInputProps("url")}
|
{...form.getInputProps("url")}
|
||||||
/>
|
/>
|
||||||
@@ -119,6 +120,7 @@ export const NewIntegrationForm = ({
|
|||||||
)}
|
)}
|
||||||
{form.values.secrets.map(({ kind }, index) => (
|
{form.values.secrets.map(({ kind }, index) => (
|
||||||
<IntegrationSecretInput
|
<IntegrationSecretInput
|
||||||
|
withAsterisk
|
||||||
key={kind}
|
key={kind}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
{...form.getInputProps(`secrets.${index}.value`)}
|
{...form.getInputProps(`secrets.${index}.value`)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button, Group, Stack, TextInput } from "@mantine/core";
|
|||||||
|
|
||||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -38,14 +38,11 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useForm({
|
const form = useZodForm(validation.user.editProfile.omit({ id: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: user.name ?? "",
|
name: user.name ?? "",
|
||||||
email: user.email ?? "",
|
email: user.email ?? "",
|
||||||
},
|
},
|
||||||
validate: zodResolver(validation.user.editProfile.omit({ id: true })),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button, Fieldset, Group, PasswordInput, Stack } from "@mantine/core";
|
|||||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
@@ -37,15 +37,13 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.user.changePassword, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
previousPassword: "",
|
/* Require previous password if the current user want's to change his password */
|
||||||
|
previousPassword: session?.user.id === user.id ? "" : "_",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
},
|
},
|
||||||
validate: zodResolver(validation.user.changePassword),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: FormType) => {
|
const handleSubmit = (values: FormType) => {
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
import { IconUserCheck } from "@tabler/icons-react";
|
import { IconUserCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { validation, z } from "@homarr/validation";
|
import { validation, z } from "@homarr/validation";
|
||||||
|
import { createCustomErrorParams } from "@homarr/validation/form";
|
||||||
|
|
||||||
import { StepperNavigationComponent } from "./stepper-navigation.component";
|
import { StepperNavigationComponent } from "./stepper-navigation.component";
|
||||||
|
|
||||||
@@ -40,40 +41,36 @@ export const UserCreateStepperComponent = () => {
|
|||||||
|
|
||||||
const { mutateAsync, isPending } = clientApi.user.create.useMutation();
|
const { mutateAsync, isPending } = clientApi.user.create.useMutation();
|
||||||
|
|
||||||
const generalForm = useForm({
|
const generalForm = useZodForm(
|
||||||
initialValues: {
|
z.object({
|
||||||
username: "",
|
username: z.string().min(1),
|
||||||
email: undefined,
|
email: z.string().email().or(z.string().length(0).optional()),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
initialValues: {
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validate: zodResolver(
|
);
|
||||||
z.object({
|
|
||||||
username: z.string().min(1),
|
|
||||||
email: z.string().email().or(z.string().length(0).optional()),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const securityForm = useForm({
|
const securityForm = useZodForm(
|
||||||
initialValues: {
|
z
|
||||||
password: "",
|
.object({
|
||||||
confirmPassword: "",
|
password: validation.user.password,
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
params: createCustomErrorParams("passwordsDoNotMatch"),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
initialValues: {
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validate: zodResolver(
|
);
|
||||||
z
|
|
||||||
.object({
|
|
||||||
password: validation.user.password,
|
|
||||||
confirmPassword: z.string(),
|
|
||||||
})
|
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
|
||||||
path: ["confirmPassword"],
|
|
||||||
message: "Passwords do not match",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allForms = useMemo(
|
const allForms = useMemo(
|
||||||
() => [generalForm, securityForm],
|
() => [generalForm, securityForm],
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { useCallback } from "react";
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ interface RenameGroupFormProps {
|
|||||||
export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: group.name,
|
name: group.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { useCallback } from "react";
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal, useModalAction } from "@homarr/modals";
|
import { createModal, useModalAction } from "@homarr/modals";
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
} from "@homarr/notifications";
|
} from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export const AddGroup = () => {
|
|||||||
const AddGroupModal = createModal<void>(({ actions }) => {
|
const AddGroupModal = createModal<void>(({ actions }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { mutate, isPending } = clientApi.group.createGroup.useMutation();
|
const { mutate, isPending } = clientApi.group.createGroup.useMutation();
|
||||||
const form = useForm({
|
const form = useZodForm(validation.group.create, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { validation, z } from "@homarr/validation";
|
import type { z } from "@homarr/validation";
|
||||||
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,7 +27,7 @@ export const BoardRenameModal = createModal<InnerProps>(
|
|||||||
void utils.board.getDefaultBoard.invalidate();
|
void utils.board.getDefaultBoard.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const form = useForm<FormType>({
|
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: innerProps.previousName,
|
name: innerProps.previousName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { useForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,7 +19,7 @@ interface InnerProps {
|
|||||||
export const CategoryEditModal = createModal<InnerProps>(
|
export const CategoryEditModal = createModal<InnerProps>(
|
||||||
({ actions, innerProps }) => {
|
({ actions, innerProps }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const form = useForm({
|
const form = useZodForm(z.object({ name: z.string().min(1) }), {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: innerProps.category.name,
|
name: innerProps.category.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { FocusEventHandler } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Combobox,
|
Combobox,
|
||||||
@@ -15,9 +16,18 @@ import { useScopedI18n } from "@homarr/translation/client";
|
|||||||
interface IconPickerProps {
|
interface IconPickerProps {
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
onChange: (iconUrl: string) => void;
|
onChange: (iconUrl: string) => void;
|
||||||
|
error?: string | null;
|
||||||
|
onFocus?: FocusEventHandler;
|
||||||
|
onBlur?: FocusEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconPicker = ({ initialValue, onChange }: IconPickerProps) => {
|
export const IconPicker = ({
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
}: IconPickerProps) => {
|
||||||
const [value, setValue] = useState<string>(initialValue ?? "");
|
const [value, setValue] = useState<string>(initialValue ?? "");
|
||||||
const [search, setSearch] = useState(initialValue ?? "");
|
const [search, setSearch] = useState(initialValue ?? "");
|
||||||
|
|
||||||
@@ -76,13 +86,18 @@ export const IconPicker = ({ initialValue, onChange }: IconPickerProps) => {
|
|||||||
setSearch(event.currentTarget.value);
|
setSearch(event.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
onClick={() => combobox.openDropdown()}
|
onClick={() => combobox.openDropdown()}
|
||||||
onFocus={() => combobox.openDropdown()}
|
onFocus={(event) => {
|
||||||
onBlur={() => {
|
onFocus?.(event);
|
||||||
|
combobox.openDropdown();
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
onBlur?.(event);
|
||||||
combobox.closeDropdown();
|
combobox.closeDropdown();
|
||||||
setSearch(value || "");
|
setSearch(value || "");
|
||||||
}}
|
}}
|
||||||
rightSectionPointerEvents="none"
|
rightSectionPointerEvents="none"
|
||||||
withAsterisk
|
withAsterisk
|
||||||
|
error={error}
|
||||||
label="Icon URL"
|
label="Icon URL"
|
||||||
/>
|
/>
|
||||||
</Combobox.Target>
|
</Combobox.Target>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
import { boardSchemas } from "node_modules/@homarr/validation/src/board";
|
import { boardSchemas } from "node_modules/@homarr/validation/src/board";
|
||||||
|
|
||||||
import { useForm, zodResolver } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { z } from "@homarr/validation";
|
import { z } from "@homarr/validation";
|
||||||
|
import { createCustomErrorParams } from "@homarr/validation/form";
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
boardNames: string[];
|
boardNames: string[];
|
||||||
@@ -14,20 +15,21 @@ interface InnerProps {
|
|||||||
export const AddBoardModal = createModal<InnerProps>(
|
export const AddBoardModal = createModal<InnerProps>(
|
||||||
({ actions, innerProps }) => {
|
({ actions, innerProps }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const form = useForm({
|
const form = useZodForm(
|
||||||
initialValues: {
|
z.object({
|
||||||
name: "",
|
name: boardSchemas.byName.shape.name.refine(
|
||||||
|
(value) => !innerProps.boardNames.includes(value),
|
||||||
|
{
|
||||||
|
params: createCustomErrorParams("boardAlreadyExists"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validate: zodResolver(
|
);
|
||||||
z.object({
|
|
||||||
name: boardSchemas.byName.shape.name.refine(
|
|
||||||
(value) => !innerProps.boardNames.includes(value),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
validateInputOnChange: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -183,7 +183,9 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
savePartialBoardSettings: protectedProcedure
|
savePartialBoardSettings: protectedProcedure
|
||||||
.input(validation.board.savePartialSettings)
|
.input(
|
||||||
|
validation.board.savePartialSettings.and(z.object({ id: z.string() })),
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await throwIfActionForbiddenAsync(
|
await throwIfActionForbiddenAsync(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ describe("initUser should initialize the first user", () => {
|
|||||||
confirmPassword: "12345679",
|
confirmPassword: "12345679",
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(actAsync()).rejects.toThrow("Passwords do not match");
|
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not create a user if the password is too short", async () => {
|
it("should not create a user if the password is too short", async () => {
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ const createUserAsync = async (
|
|||||||
await db.insert(schema.users).values({
|
await db.insert(schema.users).values({
|
||||||
id: userId,
|
id: userId,
|
||||||
name: input.username,
|
name: input.username,
|
||||||
|
email: input.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt,
|
salt,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/form": "^7.9.2"
|
"@mantine/form": "^7.9.2",
|
||||||
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
|
"@homarr/translation": "workspace:^0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,33 @@
|
|||||||
export const name = "form";
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
|
||||||
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { z } from "@homarr/validation";
|
||||||
|
import type {
|
||||||
|
AnyZodObject,
|
||||||
|
ZodEffects,
|
||||||
|
ZodIntersection,
|
||||||
|
} from "@homarr/validation";
|
||||||
|
import { zodErrorMap } from "@homarr/validation/form";
|
||||||
|
|
||||||
|
export const useZodForm = <
|
||||||
|
TSchema extends
|
||||||
|
| AnyZodObject
|
||||||
|
| ZodEffects<AnyZodObject>
|
||||||
|
| ZodIntersection<AnyZodObject, AnyZodObject>,
|
||||||
|
>(
|
||||||
|
schema: TSchema,
|
||||||
|
options: Omit<
|
||||||
|
Exclude<Parameters<typeof useForm<z.infer<TSchema>>>[0], undefined>,
|
||||||
|
"validate" | "validateInputOnBlur" | "validateInputOnChange"
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
z.setErrorMap(zodErrorMap(t));
|
||||||
|
return useForm<z.infer<TSchema>>({
|
||||||
|
...options,
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
validateInputOnChange: true,
|
||||||
|
validate: zodResolver(schema),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
110
packages/form/src/messages.ts
Normal file
110
packages/form/src/messages.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
|
import type {
|
||||||
|
ErrorMapCtx,
|
||||||
|
z,
|
||||||
|
ZodTooBigIssue,
|
||||||
|
ZodTooSmallIssue,
|
||||||
|
} from "@homarr/validation";
|
||||||
|
import { ZodIssueCode } from "@homarr/validation";
|
||||||
|
|
||||||
|
const handleStringError = (issue: z.ZodInvalidStringIssue) => {
|
||||||
|
if (typeof issue.validation === "object") {
|
||||||
|
// Check if object contains startsWith / endsWith key to determine the error type. If not, it's an includes error. (see type of issue.validation)
|
||||||
|
if ("startsWith" in issue.validation) {
|
||||||
|
return {
|
||||||
|
key: "errors.string.startsWith",
|
||||||
|
params: {
|
||||||
|
startsWith: issue.validation.startsWith,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
} else if ("endsWith" in issue.validation) {
|
||||||
|
return {
|
||||||
|
key: "errors.string.endsWith",
|
||||||
|
params: {
|
||||||
|
endsWith: issue.validation.endsWith,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: "errors.invalid_string.includes",
|
||||||
|
params: {
|
||||||
|
includes: issue.validation.includes,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooSmallError = (issue: ZodTooSmallIssue) => {
|
||||||
|
if (issue.type !== "string" && issue.type !== "number") {
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `errors.tooSmall.${issue.type}`,
|
||||||
|
params: {
|
||||||
|
minimum: issue.minimum,
|
||||||
|
count: issue.minimum,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooBigError = (issue: ZodTooBigIssue) => {
|
||||||
|
if (issue.type !== "string" && issue.type !== "number") {
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `errors.tooBig.${issue.type}`,
|
||||||
|
params: {
|
||||||
|
maximum: issue.maximum,
|
||||||
|
count: issue.maximum,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleZodError = (
|
||||||
|
issue: z.ZodIssueOptionalMessage,
|
||||||
|
ctx: ErrorMapCtx,
|
||||||
|
) => {
|
||||||
|
if (ctx.defaultError === "Required") {
|
||||||
|
return {
|
||||||
|
key: "errors.required",
|
||||||
|
params: {},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.invalid_string) {
|
||||||
|
return handleStringError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.too_small) {
|
||||||
|
return handleTooSmallError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.too_big) {
|
||||||
|
return handleTooBigError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
|
||||||
|
const { i18n } = issue.params as CustomErrorParams;
|
||||||
|
return {
|
||||||
|
key: `errors.custom.${i18n.key}`,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CustomErrorParams {
|
||||||
|
i18n: {
|
||||||
|
key: keyof TranslationObject["common"]["zod"]["errors"]["custom"];
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -512,6 +512,31 @@ export default {
|
|||||||
show: "Show preview",
|
show: "Show preview",
|
||||||
hide: "Hide preview",
|
hide: "Hide preview",
|
||||||
},
|
},
|
||||||
|
zod: {
|
||||||
|
errors: {
|
||||||
|
default: "This field is invalid",
|
||||||
|
required: "This field is required",
|
||||||
|
string: {
|
||||||
|
startsWith: "This field must start with {startsWith}",
|
||||||
|
endsWith: "This field must end with {endsWith}",
|
||||||
|
includes: "This field must include {includes}",
|
||||||
|
invalidEmail: "This field must be a valid email",
|
||||||
|
},
|
||||||
|
tooSmall: {
|
||||||
|
string: "This field must be at least {minimum} characters long",
|
||||||
|
number: "This field must be greater than or equal to {minimum}",
|
||||||
|
},
|
||||||
|
tooBig: {
|
||||||
|
string: "This field must be at most {maximum} characters long",
|
||||||
|
number: "This field must be less than or equal to {maximum}",
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
passwordsDoNotMatch: "Passwords do not match",
|
||||||
|
boardAlreadyExists: "A board with this name already exists",
|
||||||
|
// TODO: Add custom error messages
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
category: {
|
category: {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts",
|
||||||
|
"./form": "./src/form/i18n.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"@homarr/definitions": "workspace:^0.1.0"
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
|
"@homarr/translation": "workspace:^0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const boardNameSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(255)
|
.max(255)
|
||||||
.regex(/^[A-Za-z0-9-\\._]+$/);
|
.regex(/^[A-Za-z0-9-\\._]*$/);
|
||||||
|
|
||||||
const byNameSchema = z.object({
|
const byNameSchema = z.object({
|
||||||
name: boardNameSchema,
|
name: boardNameSchema,
|
||||||
@@ -53,12 +53,7 @@ const savePartialSettingsSchema = z
|
|||||||
customCss: z.string().max(16384),
|
customCss: z.string().max(16384),
|
||||||
columnCount: z.number().min(1).max(24),
|
columnCount: z.number().min(1).max(24),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial();
|
||||||
.and(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveSchema = z.object({
|
const saveSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|||||||
139
packages/validation/src/form/i18n.ts
Normal file
139
packages/validation/src/form/i18n.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
|
||||||
|
import { ZodIssueCode } from "zod";
|
||||||
|
|
||||||
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
|
|
||||||
|
export const zodErrorMap = <
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
TFunction extends (key: string, ...params: any[]) => string,
|
||||||
|
>(
|
||||||
|
t: TFunction,
|
||||||
|
) => {
|
||||||
|
return (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
|
||||||
|
const error = handleZodError(issue, ctx);
|
||||||
|
if ("message" in error && error.message)
|
||||||
|
return {
|
||||||
|
message: error.message ?? ctx.defaultError,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
message: t(
|
||||||
|
error.key ? `common.zod.${error.key}` : "common.zod.errors.default",
|
||||||
|
error.params ?? {},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStringError = (issue: z.ZodInvalidStringIssue) => {
|
||||||
|
if (issue.validation === "email") {
|
||||||
|
return {
|
||||||
|
key: "errors.string.invalidEmail",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof issue.validation === "object") {
|
||||||
|
if ("startsWith" in issue.validation) {
|
||||||
|
return {
|
||||||
|
key: "errors.string.startsWith",
|
||||||
|
params: {
|
||||||
|
startsWith: issue.validation.startsWith,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
} else if ("endsWith" in issue.validation) {
|
||||||
|
return {
|
||||||
|
key: "errors.string.endsWith",
|
||||||
|
params: {
|
||||||
|
endsWith: issue.validation.endsWith,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: "errors.invalid_string.includes",
|
||||||
|
params: {
|
||||||
|
includes: issue.validation.includes,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooSmallError = (issue: ZodTooSmallIssue) => {
|
||||||
|
if (issue.type !== "string" && issue.type !== "number") {
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issue.type === "string" && issue.minimum === 1) {
|
||||||
|
return {
|
||||||
|
key: "errors.required",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `errors.tooSmall.${issue.type}`,
|
||||||
|
params: {
|
||||||
|
minimum: issue.minimum,
|
||||||
|
count: issue.minimum,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooBigError = (issue: ZodTooBigIssue) => {
|
||||||
|
if (issue.type !== "string" && issue.type !== "number") {
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `errors.tooBig.${issue.type}`,
|
||||||
|
params: {
|
||||||
|
maximum: issue.maximum,
|
||||||
|
count: issue.maximum,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZodError = (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
|
||||||
|
if (ctx.defaultError === "Required") {
|
||||||
|
return {
|
||||||
|
key: "errors.required",
|
||||||
|
params: {},
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.invalid_string) {
|
||||||
|
return handleStringError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.too_small) {
|
||||||
|
return handleTooSmallError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.too_big) {
|
||||||
|
return handleTooBigError(issue);
|
||||||
|
}
|
||||||
|
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
|
||||||
|
const { i18n } = issue.params as CustomErrorParams;
|
||||||
|
return {
|
||||||
|
key: `errors.custom.${i18n.key}`,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CustomErrorParams {
|
||||||
|
i18n: {
|
||||||
|
key: keyof TranslationObject["common"]["zod"]["errors"]["custom"];
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCustomErrorParams = (
|
||||||
|
i18n: CustomErrorParams["i18n"] | CustomErrorParams["i18n"]["key"],
|
||||||
|
) => (typeof i18n === "string" ? { i18n: { key: i18n } } : { i18n });
|
||||||
@@ -1,25 +1,34 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createCustomErrorParams } from "./form/i18n";
|
||||||
|
|
||||||
const usernameSchema = z.string().min(3).max(255);
|
const usernameSchema = z.string().min(3).max(255);
|
||||||
const passwordSchema = z.string().min(8).max(255);
|
const passwordSchema = z.string().min(8).max(255);
|
||||||
|
|
||||||
|
const confirmPasswordRefine = [
|
||||||
|
(data: { password: string; confirmPassword: string }) =>
|
||||||
|
data.password === data.confirmPassword,
|
||||||
|
{
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
params: createCustomErrorParams("passwordsDoNotMatch"),
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
] satisfies [(args: any) => boolean, unknown];
|
||||||
|
|
||||||
const createUserSchema = z
|
const createUserSchema = z
|
||||||
.object({
|
.object({
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().or(z.string().length(0).optional()),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
|
||||||
path: ["confirmPassword"],
|
|
||||||
message: "Passwords do not match",
|
|
||||||
});
|
|
||||||
|
|
||||||
const initUserSchema = createUserSchema;
|
const initUserSchema = createUserSchema;
|
||||||
|
|
||||||
const signInSchema = z.object({
|
const signInSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string().min(1),
|
||||||
password: z.string(),
|
password: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
const registrationSchema = z
|
const registrationSchema = z
|
||||||
@@ -28,10 +37,7 @@ const registrationSchema = z
|
|||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
|
||||||
path: ["confirmPassword"],
|
|
||||||
message: "Passwords do not match",
|
|
||||||
});
|
|
||||||
|
|
||||||
const registrationSchemaApi = registrationSchema.and(
|
const registrationSchemaApi = registrationSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -54,14 +60,11 @@ const editProfileSchema = z.object({
|
|||||||
|
|
||||||
const changePasswordSchema = z
|
const changePasswordSchema = z
|
||||||
.object({
|
.object({
|
||||||
previousPassword: z.string(),
|
previousPassword: z.string().min(1),
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine(confirmPasswordRefine[0], confirmPasswordRefine[1]);
|
||||||
path: ["confirmPassword"],
|
|
||||||
message: "Passwords do not match",
|
|
||||||
});
|
|
||||||
|
|
||||||
const changePasswordApiSchema = changePasswordSchema.and(
|
const changePasswordApiSchema = changePasswordSchema.and(
|
||||||
z.object({ userId: z.string() }),
|
z.object({ userId: z.string() }),
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -556,6 +556,12 @@ importers:
|
|||||||
|
|
||||||
packages/form:
|
packages/form:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@homarr/translation':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../translation
|
||||||
|
'@homarr/validation':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../validation
|
||||||
'@mantine/form':
|
'@mantine/form':
|
||||||
specifier: ^7.9.2
|
specifier: ^7.9.2
|
||||||
version: 7.9.2(react@18.3.1)
|
version: 7.9.2(react@18.3.1)
|
||||||
@@ -787,6 +793,9 @@ importers:
|
|||||||
'@homarr/definitions':
|
'@homarr/definitions':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../definitions
|
version: link:../definitions
|
||||||
|
'@homarr/translation':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../translation
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.23.8
|
specifier: ^3.23.8
|
||||||
version: 3.23.8
|
version: 3.23.8
|
||||||
|
|||||||
Reference in New Issue
Block a user