From dfed804f65003f715ed0bf699de39b4c8c63d63b Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 18 May 2024 16:55:08 +0200 Subject: [PATCH] feat: add i18n translated form errors (#509) --- .../auth/invite/[id]/_registration-form.tsx | 13 +- .../app/[locale]/auth/login/_login-form.tsx | 11 +- .../boards/[name]/settings/_background.tsx | 5 +- .../boards/[name]/settings/_colors.tsx | 14 +- .../boards/[name]/settings/_general.tsx | 39 +++-- .../boards/[name]/settings/_layout.tsx | 14 +- .../[locale]/init/user/_init-user-form.tsx | 7 +- .../src/app/[locale]/manage/apps/_form.tsx | 9 +- .../_integration-secret-inputs.tsx | 1 + .../edit/[id]/_integration-edit-form.tsx | 7 +- .../new/_integration-new-form.tsx | 8 +- .../general/_components/_profile-form.tsx | 7 +- .../_components/_change-password-form.tsx | 10 +- .../_components/create-user-stepper.tsx | 61 ++++---- .../users/groups/[id]/_rename-group-form.tsx | 5 +- .../manage/users/groups/_add-group.tsx | 5 +- .../board/modals/board-rename-modal.tsx | 7 +- .../sections/category/category-edit-modal.tsx | 5 +- .../components/icons/picker/icon-picker.tsx | 21 ++- .../manage/boards/add-board-modal.tsx | 30 ++-- packages/api/src/router/board.ts | 4 +- packages/api/src/router/test/user.spec.ts | 2 +- packages/api/src/router/user.ts | 1 + packages/form/package.json | 4 +- packages/form/src/index.ts | 34 ++++- packages/form/src/messages.ts | 110 ++++++++++++++ packages/translation/src/lang/en.ts | 25 ++++ packages/validation/package.json | 6 +- packages/validation/src/board.ts | 9 +- packages/validation/src/form/i18n.ts | 139 ++++++++++++++++++ packages/validation/src/user.ts | 35 +++-- pnpm-lock.yaml | 9 ++ 32 files changed, 501 insertions(+), 156 deletions(-) create mode 100644 packages/form/src/messages.ts create mode 100644 packages/validation/src/form/i18n.ts diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx index f7ea9ace5..9baf60958 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation"; import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; -import { useForm, zodResolver } from "@homarr/form"; +import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification, @@ -24,18 +24,17 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => { const t = useScopedI18n("user"); const router = useRouter(); const { mutate, isPending } = clientApi.user.register.useMutation(); - const form = useForm({ - validate: zodResolver(validation.user.registration), + const form = useZodForm(validation.user.registration, { initialValues: { username: "", password: "", confirmPassword: "", }, - validateInputOnBlur: true, - validateInputOnChange: true, }); - const handleSubmit = (values: FormType) => { + const handleSubmit = ( + values: z.infer, + ) => { mutate( { ...values, @@ -88,5 +87,3 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => { ); }; - -type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx index ec5edf3a5..d98d387b0 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx @@ -13,7 +13,7 @@ import { import { IconAlertTriangle } from "@tabler/icons-react"; import { signIn } from "@homarr/auth/client"; -import { useForm, zodResolver } from "@homarr/form"; +import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification, @@ -27,15 +27,16 @@ export const LoginForm = () => { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - const form = useForm({ - validate: zodResolver(validation.user.signIn), + const form = useZodForm(validation.user.signIn, { initialValues: { name: "", password: "", }, }); - const handleSubmitAsync = async (values: FormType) => { + const handleSubmitAsync = async ( + values: z.infer, + ) => { setIsLoading(true); setError(undefined); await signIn("credentials", { @@ -92,5 +93,3 @@ export const LoginForm = () => { ); }; - -type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx index 08c936336..a60f81395 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx @@ -7,11 +7,12 @@ import { backgroundImageRepeats, backgroundImageSizes, } from "@homarr/definitions"; -import { useForm } from "@homarr/form"; +import { useZodForm } from "@homarr/form"; import type { TranslationObject } from "@homarr/translation"; import { useI18n } from "@homarr/translation/client"; import type { SelectItemWithDescriptionBadge } from "@homarr/ui"; import { SelectWithDescriptionBadge } from "@homarr/ui"; +import { validation } from "@homarr/validation"; import type { Board } from "../../_types"; import { useSavePartialSettingsMutation } from "./_shared"; @@ -23,7 +24,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => { const t = useI18n(); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); - const form = useForm({ + const form = useZodForm(validation.board.savePartialSettings, { initialValues: { backgroundImageUrl: board.backgroundImageUrl ?? "", backgroundImageAttachment: board.backgroundImageAttachment, diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_colors.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_colors.tsx index 4b38a2d5f..f80511d06 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_colors.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_colors.tsx @@ -17,8 +17,9 @@ import { } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { useForm } from "@homarr/form"; +import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; +import { validation } from "@homarr/validation"; import type { Board } from "../../_types"; import { generateColors } from "../../(content)/_theme"; @@ -33,7 +34,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/; const progressPercentageLabel = (value: number) => `${value}%`; export const ColorSettingsContent = ({ board }: Props) => { - const form = useForm({ + const form = useZodForm(validation.board.savePartialSettings, { initialValues: { primaryColor: board.primaryColor, secondaryColor: board.secondaryColor, @@ -114,15 +115,16 @@ export const ColorSettingsContent = ({ board }: Props) => { }; interface ColorsPreviewProps { - previewColor: string; + previewColor: string | undefined; } const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => { const theme = useMantineTheme(); - const colors = hexRegex.test(previewColor) - ? generateColors(previewColor) - : generateColors("#000000"); + const colors = + previewColor && hexRegex.test(previewColor) + ? generateColors(previewColor) + : generateColors("#000000"); return ( diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx index d8e3eec82..a47624d9e 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx @@ -17,8 +17,9 @@ import { } from "@mantine/hooks"; import { IconAlertTriangle } from "@tabler/icons-react"; -import { useForm } from "@homarr/form"; +import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; +import { validation } from "@homarr/validation"; import type { Board } from "../../_types"; import { useUpdateBoard } from "../../(content)/_client"; @@ -38,20 +39,30 @@ export const GeneralSettingsContent = ({ board }: Props) => { const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); - const form = useForm({ - initialValues: { - pageTitle: board.pageTitle ?? "", - logoImageUrl: board.logoImageUrl ?? "", - metaTitle: board.metaTitle ?? "", - faviconImageUrl: board.faviconImageUrl ?? "", + const form = useZodForm( + validation.board.savePartialSettings + .pick({ + pageTitle: true, + logoImageUrl: true, + 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 faviconStatus = useFaviconPreview(form.values.faviconImageUrl); diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx index ea8865a7e..c4ed09d4c 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx @@ -2,8 +2,9 @@ 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 { validation } from "@homarr/validation"; import type { Board } from "../../_types"; import { useSavePartialSettingsMutation } from "./_shared"; @@ -15,11 +16,14 @@ export const LayoutSettingsContent = ({ board }: Props) => { const t = useI18n(); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); - const form = useForm({ - initialValues: { - columnCount: board.columnCount, + const form = useZodForm( + validation.board.savePartialSettings.pick({ columnCount: true }).required(), + { + initialValues: { + columnCount: board.columnCount, + }, }, - }); + ); return (
{ const t = useScopedI18n("user"); const { mutateAsync, error, isPending } = clientApi.user.initUser.useMutation(); - const form = useForm({ - validate: zodResolver(validation.user.init), - validateInputOnBlur: true, - validateInputOnChange: true, + const form = useZodForm(validation.user.init, { initialValues: { username: "", password: "", diff --git a/apps/nextjs/src/app/[locale]/manage/apps/_form.tsx b/apps/nextjs/src/app/[locale]/manage/apps/_form.tsx index d024a9c36..0b0162659 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/_form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/_form.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; 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 { useI18n } from "@homarr/translation/client"; import type { z } from "@homarr/validation"; @@ -25,14 +25,13 @@ export const AppForm = (props: AppFormProps) => { props; const t = useI18n(); - const form = useForm({ + const form = useZodForm(validation.app.manage, { initialValues: initialValues ?? { name: "", description: "", iconUrl: "", href: "", }, - validate: zodResolver(validation.app.manage), }); return ( @@ -41,9 +40,7 @@ export const AppForm = (props: AppFormProps) => { { - form.setFieldValue("iconUrl", iconUrl); - }} + {...form.getInputProps("iconUrl")} />