diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditProductNameForm.tsx b/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditProductNameForm.tsx index bb3b4b9fc1..2ed397bfa7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditProductNameForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditProductNameForm.tsx @@ -6,10 +6,10 @@ import { SubmitHandler, useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { z } from "zod"; -import { TProduct } from "@formbricks/types/product"; +import { TProduct, ZProduct } from "@formbricks/types/product"; import { Button } from "@formbricks/ui/Button"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@formbricks/ui/Form"; import { Input } from "@formbricks/ui/Input"; -import { Label } from "@formbricks/ui/Label"; import { updateProductAction } from "../actions"; @@ -19,9 +19,7 @@ type EditProductNameProps = { isProductNameEditDisabled: boolean; }; -const editProductNameSchema = z.object({ - name: z.string().trim().min(1, { message: "Product name cannot be empty" }), -}); +const editProductNameSchema = ZProduct.pick({ name: true }); type TEditProductName = z.infer; @@ -31,11 +29,7 @@ export const EditProductNameForm: React.FC = ({ isProductNameEditDisabled, }) => { const router = useRouter(); - const { - register, - handleSubmit, - formState: { isSubmitting, errors }, - } = useForm({ + const form = useForm({ defaultValues: { name: product.name, }, @@ -43,7 +37,8 @@ export const EditProductNameForm: React.FC = ({ mode: "onChange", }); - const nameError = errors.name?.message; + const nameError = form.formState.errors.name?.message; + const isSubmitting = form.formState.isSubmitting; const updateProduct: SubmitHandler = async (data) => { const name = data.name.trim(); @@ -54,7 +49,7 @@ export const EditProductNameForm: React.FC = ({ } if (name === product.name) { - toast.success("This is already your product name"); + form.setError("name", { type: "manual", message: "Product name is the same" }, { shouldFocus: true }); return; } @@ -62,7 +57,7 @@ export const EditProductNameForm: React.FC = ({ if (isProductNameEditDisabled) { toast.error("Only Owners, Admins and Editors can perform this action."); - throw new Error(); + return; } if (!!updatedProduct?.id) { @@ -76,18 +71,35 @@ export const EditProductNameForm: React.FC = ({ }; return !isProductNameEditDisabled ? ( -
- - - -
+
+ + ( + + What's your product called? + + + + + + )} + /> + + + + ) : (

Only Owners, Admins and Editors can perform this action.

); diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditWaitingTimeForm.tsx b/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditWaitingTimeForm.tsx index 1b85a1cab4..129564e253 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditWaitingTimeForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/components/EditWaitingTimeForm.tsx @@ -6,10 +6,10 @@ import { SubmitHandler, useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { z } from "zod"; -import { TProduct } from "@formbricks/types/product"; +import { TProduct, ZProduct } from "@formbricks/types/product"; import { Button } from "@formbricks/ui/Button"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@formbricks/ui/Form"; import { Input } from "@formbricks/ui/Input"; -import { Label } from "@formbricks/ui/Label"; import { updateProductAction } from "../actions"; @@ -18,22 +18,14 @@ type EditWaitingTimeProps = { product: TProduct; }; -const editWaitingTimeSchema = z.object({ - recontactDays: z - .number({ message: "Recontact days is required" }) - .min(0, { message: "Must be a positive number" }) - .max(365, { message: "Must be less than 365" }), -}); +const editWaitingTimeSchema = ZProduct.pick({ recontactDays: true }); type EditWaitingTimeFormValues = z.infer; export const EditWaitingTimeForm: React.FC = ({ product, environmentId }) => { const router = useRouter(); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ + + const form = useForm({ defaultValues: { recontactDays: product.recontactDays, }, @@ -54,26 +46,40 @@ export const EditWaitingTimeForm: React.FC = ({ product, e }; return ( -
- - + + + ( + + Wait X days before showing next survey: + + { + const value = e.target.value; + if (value === "") { + field.onChange(""); + } - {errors?.recontactDays ? ( -
-

{errors?.recontactDays?.message}

-
- ) : null} + field.onChange(parseInt(value, 10)); + }} + /> +
+ +
+ )} + /> - - + + + ); }; diff --git a/packages/types/product.ts b/packages/types/product.ts index 1820d549f6..ce3216289b 100644 --- a/packages/types/product.ts +++ b/packages/types/product.ts @@ -41,10 +41,14 @@ export const ZProduct = z.object({ id: z.string().cuid2(), createdAt: z.date(), updatedAt: z.date(), - name: z.string(), + name: z.string().trim().min(1, { message: "Product name cannot be empty" }), teamId: z.string(), styling: ZProductStyling, - recontactDays: z.number().int(), + recontactDays: z + .number({ message: "Recontact days is required" }) + .int() + .min(0, { message: "Must be a positive number" }) + .max(365, { message: "Must be less than 365" }), inAppSurveyBranding: z.boolean(), linkSurveyBranding: z.boolean(), placement: ZPlacement, diff --git a/packages/ui/Form/index.tsx b/packages/ui/Form/index.tsx new file mode 100644 index 0000000000..d743b9d7ba --- /dev/null +++ b/packages/ui/Form/index.tsx @@ -0,0 +1,147 @@ +"use client"; + +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import * as React from "react"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { cn } from "@formbricks/lib/cn"; + +import { Label } from "../Label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext({} as FormFieldContextValue); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext({} as FormItemContextValue); + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); + } +); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return