From 62c514acf2f24d36c1ed188cfcf766006b74f5b1 Mon Sep 17 00:00:00 2001 From: Hicham El Bouaaichi <77807053+beonma@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:10:46 +0100 Subject: [PATCH 1/2] feat: rework loading in Settings pages (#2650) Co-authored-by: Piyush Gupta Co-authored-by: pandeymangg --- .../(account)/notifications/loading.tsx | 65 ++++++++++ .../settings/(account)/profile/loading.tsx | 31 ++++- .../(organization)/billing/loading.tsx | 29 ++++- .../(organization)/enterprise/loading.tsx | 29 ++++- .../(organization)/members/loading.tsx | 117 ++++++++---------- .../settings/(organization)/members/page.tsx | 27 +--- 6 files changed, 196 insertions(+), 102 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx new file mode 100644 index 0000000000..8d1c171688 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx @@ -0,0 +1,65 @@ +const LoadingCard = ({ title, description, skeletonLines }) => { + return ( +
+
+

{title}

+

{description}

+
+
+
+ {skeletonLines.map((line, index) => ( +
+
+
+ ))} +
+
+
+ ); +}; + +const Loading = () => { + const cards = [ + { + title: "Email alerts (Surveys)", + description: "Set up an alert to get an email on new responses.", + skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], + }, + { + title: "Weekly summary (Products)", + description: "Stay up-to-date with a Weekly every Monday.", + skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], + }, + ]; + + const pages = ["Profile", "Notifications"]; + + return ( +
+
+
+

Account Settings

+
+
+
+
+ +
+
+
+ {cards.map((card, index) => ( + + ))} +
+ ); +}; + +export default Loading; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.tsx index d35cd41d44..69573fe125 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/loading.tsx @@ -1,12 +1,12 @@ const LoadingCard = ({ title, description, skeletonLines }) => { return ( -
-
+
+

{title}

{description}

-
+
{skeletonLines.map((line, index) => (
@@ -28,7 +28,6 @@ const Loading = () => { { classes: "h-6 w-64" }, { classes: "h-4 w-28" }, { classes: "h-6 w-64" }, - { classes: "h-8 w-24" }, ], }, { @@ -48,9 +47,29 @@ const Loading = () => { }, ]; + const pages = ["Profile", "Notifications"]; + return ( -
-

Profile

+
+
+
+

Account Settings

+
+
+
+
+ +
+
+
{cards.map((card, index) => ( ))} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx index c6924bfb0e..40b37b3d4a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/loading.tsx @@ -1,12 +1,29 @@ +const pages = ["Members", "Billing & Plan"]; + const Loading = () => { return ( -
-

Billing & Plan

-
-
-
-
+
+
+
+

Organization Settings

+
+
+
+ +
+
+
+
+
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx index 9fbd27e893..f520da8e64 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/loading.tsx @@ -1,12 +1,29 @@ +const pages = ["Members", "Enterprise License"]; + const Loading = () => { return ( -
-

Enterprise License

-
-
-
-
+
+
+
+

Organization Settings

+
+
+
+ +
+
+
+
+
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/loading.tsx index 3da0ee0aea..772cfad57c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/loading.tsx @@ -1,77 +1,68 @@ -import { Skeleton } from "@formbricks/ui/Skeleton"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -const LoadingCard = ({ - title, - description, - skeleton, -}: { - title: string; - description: string; - skeleton: React.ReactNode; -}) => { +const LoadingCard = ({ title, description, skeletonLines }) => { return ( -
-
+
+

{title}

{description}

-
{skeleton}
+
+
+ {skeletonLines.map((line, index) => ( +
+
+
+ ))} +
+
); }; +const cards = [ + { + title: "Manage members", + description: "Add or remove members in your organization.", + skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-8 w-80" }, { classes: "h-8 w-80" }], + }, + { + title: "Organization Name", + description: "Give your organization a descriptive name.", + skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-8 w-80" }], + }, + { + title: "Delete Organization", + description: + "Delete organization with all its products including all surveys, responses, people, actions and attributes", + skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-8 w-80" }], + }, +]; + +const pages = ["Members", IS_FORMBRICKS_CLOUD ? "Billing & Plan" : "Enterprise License"]; + const Loading = () => { - const cards = [ - { - title: "Manage members", - description: "Add or remove members in your organization", - skeleton: ( -
-
- - -
- -
-
-
-
Fullname
-
Email
-
Role
-
-
- -
-
-
- ), - }, - { - title: "Organization Name", - description: "Give your organization a descriptive name", - skeleton: ( -
- - - -
- ), - }, - { - title: "Delete account", - description: "Delete your account with all of your personal information and data.", - skeleton: ( -
- - -
- ), - }, - ]; - return ( -
-

Profile

+
+
+
+

Organization Settings

+
+
+
+
+ +
+
+
{cards.map((card, index) => ( ))} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx index dc843d6443..35cca19590 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx @@ -15,7 +15,6 @@ import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/ser import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper"; import { PageHeader } from "@formbricks/ui/PageHeader"; import { SettingsId } from "@formbricks/ui/SettingsId"; -import { Skeleton } from "@formbricks/ui/Skeleton"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteOrganization } from "./components/DeleteOrganization"; @@ -23,26 +22,12 @@ import { EditMemberships } from "./components/EditMemberships"; import { EditOrganizationName } from "./components/EditOrganizationName"; const MembersLoading = () => ( -
-
-
-
Fullname
-
Email
-
Role
-
- -
- {[1, 2, 3].map((i) => ( -
- - - - -
- ))} -
+
+ {Array.from(Array(2)).map((_, index) => ( +
+
+
+ ))}
); From 4e39f4544674deb03332879d93982ab84ae63086 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Mon, 3 Jun 2024 20:56:30 +0530 Subject: [PATCH 2/2] fix: settings forms (#2700) Co-authored-by: Matti Nannt --- .../profile/components/EditAvatar.tsx | 111 ------------ .../(account)/profile/components/EditName.tsx | 73 -------- .../components/EditProfileAvatarForm.tsx | 171 ++++++++++++++++++ .../components/EditProfileDetailsForm.tsx | 82 +++++++++ .../settings/(account)/profile/page.tsx | 8 +- .../components/EditOrganizationName.tsx | 96 ---------- .../components/EditOrganizationNameForm.tsx | 90 +++++++++ .../settings/(organization)/members/page.tsx | 4 +- .../migration.sql | 8 + packages/database/schema.prisma | 2 +- packages/types/organizations.ts | 5 +- packages/types/user.ts | 12 +- 12 files changed, 371 insertions(+), 291 deletions(-) delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditAvatar.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditName.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationName.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationNameForm.tsx create mode 100644 packages/database/migrations/20240530073540_made_name_property_on_user_model_optional/migration.sql diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditAvatar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditAvatar.tsx deleted file mode 100644 index 8c1ce2c298..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditAvatar.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { - removeAvatarAction, - updateAvatarAction, -} from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions"; -import { handleFileUpload } from "@/app/lib/fileUpload"; -import { Session } from "next-auth"; -import { useRouter } from "next/navigation"; -import { useRef, useState } from "react"; -import toast from "react-hot-toast"; - -import { ProfileAvatar } from "@formbricks/ui/Avatars"; -import { Button } from "@formbricks/ui/Button"; - -export const EditAvatar = ({ session, environmentId }: { session: Session; environmentId: string }) => { - const inputRef = useRef(null); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - const handleUpload = async (file: File, environmentId: string) => { - setIsLoading(true); - try { - if (session?.user.imageUrl) { - // If avatar image already exist, then remove it before update action - await removeAvatarAction(environmentId); - } - const { url, error } = await handleFileUpload(file, environmentId); - - if (error) { - toast.error(error); - setIsLoading(false); - return; - } - - await updateAvatarAction(url); - router.refresh(); - } catch (err) { - toast.error("Avatar update failed. Please try again."); - setIsLoading(false); - } - - setIsLoading(false); - }; - - const handleRemove = async () => { - setIsLoading(true); - - try { - await removeAvatarAction(environmentId); - } catch (err) { - toast.error("Avatar update failed. Please try again."); - } finally { - setIsLoading(false); - if (inputRef.current) { - inputRef.current.value = ""; - } - } - }; - - return ( -
-
- {isLoading && ( -
- - - - -
- )} - - -
- -
- - {session?.user?.imageUrl && ( - - )} -
-
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditName.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditName.tsx deleted file mode 100644 index b972e4ec6a..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditName.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { SubmitHandler, useForm } from "react-hook-form"; -import toast from "react-hot-toast"; - -import { TUser } from "@formbricks/types/user"; -import { Button } from "@formbricks/ui/Button"; -import { Input } from "@formbricks/ui/Input"; -import { Label } from "@formbricks/ui/Label"; - -import { updateUserAction } from "../actions"; - -type FormData = { - name: string; -}; - -export const EditName = ({ user }: { user: TUser }) => { - const { - register, - handleSubmit, - formState: { isSubmitting }, - watch, - } = useForm(); - - const nameValue = watch("name", user.name || ""); - const isNotEmptySpaces = (value: string) => value.trim() !== ""; - - const onSubmit: SubmitHandler = async (data) => { - try { - data.name = data.name.trim(); - if (!isNotEmptySpaces(data.name)) { - toast.error("Please enter at least one character"); - return; - } - if (data.name === user.name) { - toast.success("This is already your name"); - return; - } - await updateUserAction({ name: data.name }); - toast.success("Your name was updated successfully"); - } catch (error) { - toast.error(`Error: ${error.message}`); - } - }; - - return ( - <> -
- - - -
- - -
- -
- - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx new file mode 100644 index 0000000000..610cc0fb51 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { + removeAvatarAction, + updateAvatarAction, +} from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions"; +import { handleFileUpload } from "@/app/lib/fileUpload"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Session } from "next-auth"; +import { useRouter } from "next/navigation"; +import { useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; + +import { ProfileAvatar } from "@formbricks/ui/Avatars"; +import { Button } from "@formbricks/ui/Button"; +import { FormError, FormField, FormItem, FormProvider } from "@formbricks/ui/Form"; + +interface EditProfileAvatarFormProps { + session: Session; + environmentId: string; +} + +export const EditProfileAvatarForm = ({ session, environmentId }: EditProfileAvatarFormProps) => { + const inputRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const fileSchema = + typeof window !== "undefined" + ? z + .instanceof(FileList) + .refine((files) => files.length === 1, "You must select a file.") + .refine((files) => { + const file = files[0]; + const allowedTypes = ["image/jpeg", "image/png"]; + return allowedTypes.includes(file.type); + }, "Invalid file type. Only JPEG and PNG are allowed.") + .refine((files) => { + const file = files[0]; + const maxSize = 10 * 1024 * 1024; + return file.size <= maxSize; + }, "File size must be less than 10MB.") + : z.any(); + + const formSchema = z.object({ + file: fileSchema, + }); + + type FormValues = z.infer; + + const form = useForm({ + mode: "onChange", + resolver: zodResolver(formSchema), + }); + + const handleUpload = async (file: File, environmentId: string) => { + setIsLoading(true); + try { + if (session?.user.imageUrl) { + // If avatar image already exists, then remove it before update action + await removeAvatarAction(environmentId); + } + const { url, error } = await handleFileUpload(file, environmentId); + + if (error) { + toast.error(error); + setIsLoading(false); + return; + } + + await updateAvatarAction(url); + router.refresh(); + } catch (err) { + toast.error("Avatar update failed. Please try again."); + setIsLoading(false); + } + + setIsLoading(false); + }; + + const handleRemove = async () => { + setIsLoading(true); + + try { + await removeAvatarAction(environmentId); + } catch (err) { + toast.error("Avatar update failed. Please try again."); + } finally { + setIsLoading(false); + form.reset(); + } + }; + + const onSubmit = async (data: FormValues) => { + const file = data.file[0]; + if (file) { + await handleUpload(file, environmentId); + } + }; + + return ( +
+
+ {isLoading && ( +
+ + + + +
+ )} + + +
+ + +
+ ( + +
+ + + {session?.user?.imageUrl && ( + + )} +
+ + +
+ )} + /> + +
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx new file mode 100644 index 0000000000..4920382d1a --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; + +import { TUser, ZUser } from "@formbricks/types/user"; +import { Button } from "@formbricks/ui/Button"; +import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form"; +import { Input } from "@formbricks/ui/Input"; +import { Label } from "@formbricks/ui/Label"; + +import { updateUserAction } from "../actions"; + +const ZEditProfileNameFormSchema = ZUser.pick({ name: true }); +type TEditProfileNameForm = z.infer; + +export const EditProfileDetailsForm = ({ user }: { user: TUser }) => { + const form = useForm({ + defaultValues: { name: user.name }, + mode: "onChange", + resolver: zodResolver(ZEditProfileNameFormSchema), + }); + + const { isSubmitting, isDirty } = form.formState; + + const onSubmit: SubmitHandler = async (data) => { + try { + const name = data.name.trim(); + await updateUserAction({ name }); + toast.success("Your name was updated successfully"); + + form.reset({ name }); + } catch (error) { + toast.error(`Error: ${error.message}`); + } + }; + + return ( + +
+ ( + + Full Name + + + + + + + )} + /> + + {/* disabled */} +
+ + +
+ + + +
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx index 83e0d91cdd..0cc713f2c0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx @@ -11,8 +11,8 @@ import { SettingsId } from "@formbricks/ui/SettingsId"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteAccount } from "./components/DeleteAccount"; -import { EditAvatar } from "./components/EditAvatar"; -import { EditName } from "./components/EditName"; +import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm"; +import { EditProfileDetailsForm } from "./components/EditProfileDetailsForm"; const Page = async ({ params }: { params: { environmentId: string } }) => { const { environmentId } = params; @@ -30,12 +30,12 @@ const Page = async ({ params }: { params: { environmentId: string } }) => { {user && (
- + - + {user.identityProvider === "email" && ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationName.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationName.tsx deleted file mode 100644 index 92c8f6c8ed..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationName.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { SubmitHandler, useForm, useWatch } from "react-hook-form"; -import toast from "react-hot-toast"; - -import { getAccessFlags } from "@formbricks/lib/membership/utils"; -import { TMembershipRole } from "@formbricks/types/memberships"; -import { TOrganization } from "@formbricks/types/organizations"; -import { Button } from "@formbricks/ui/Button"; -import { Input } from "@formbricks/ui/Input"; -import { Label } from "@formbricks/ui/Label"; - -interface EditOrganizationNameForm { - name: string; -} - -interface EditOrganizationNameProps { - environmentId: string; - organization: TOrganization; - membershipRole?: TMembershipRole; -} - -export const EditOrganizationName = ({ organization, membershipRole }: EditOrganizationNameProps) => { - const router = useRouter(); - const { - register, - control, - handleSubmit, - formState: { errors }, - } = useForm({ - defaultValues: { - name: organization.name, - }, - }); - const [isUpdatingOrganization, setIsUpdatingOrganization] = useState(false); - const { isViewer } = getAccessFlags(membershipRole); - - const organizationName = useWatch({ - control, - name: "name", - }); - - const isOrganizationNameInputEmpty = !organizationName?.trim(); - const currentOrganizationName = organizationName?.trim().toLowerCase() ?? ""; - const previousOrganizationName = organization?.name?.trim().toLowerCase() ?? ""; - - const handleUpdateOrganizationName: SubmitHandler = async (data) => { - try { - data.name = data.name.trim(); - setIsUpdatingOrganization(true); - await updateOrganizationNameAction(organization.id, data.name); - - setIsUpdatingOrganization(false); - toast.success("Organization name updated successfully."); - - router.refresh(); - } catch (err) { - setIsUpdatingOrganization(false); - toast.error(`Error: ${err.message}`); - } - }; - - return isViewer ? ( -

You are not authorized to perform this action.

- ) : ( -
- - - - {errors?.name?.message &&

{errors.name.message}

} - - -
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationNameForm.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationNameForm.tsx new file mode 100644 index 0000000000..418ebb840a --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/components/EditOrganizationNameForm.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/members/actions"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; + +import { getAccessFlags } from "@formbricks/lib/membership/utils"; +import { TMembershipRole } from "@formbricks/types/memberships"; +import { TOrganization, ZOrganization } from "@formbricks/types/organizations"; +import { Button } from "@formbricks/ui/Button"; +import { FormControl, FormError, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form"; +import { Input } from "@formbricks/ui/Input"; + +interface EditOrganizationNameProps { + environmentId: string; + organization: TOrganization; + membershipRole?: TMembershipRole; +} + +const ZEditOrganizationNameFormSchema = ZOrganization.pick({ name: true }); +type EditOrganizationNameForm = z.infer; + +export const EditOrganizationNameForm = ({ organization, membershipRole }: EditOrganizationNameProps) => { + const form = useForm({ + defaultValues: { + name: organization.name, + }, + mode: "onChange", + resolver: zodResolver(ZEditOrganizationNameFormSchema), + }); + + const { isViewer } = getAccessFlags(membershipRole); + + const { isSubmitting, isDirty } = form.formState; + + const handleUpdateOrganizationName: SubmitHandler = async (data) => { + try { + const name = data.name.trim(); + const updatedOrg = await updateOrganizationNameAction(organization.id, name); + + toast.success("Organization name updated successfully."); + form.reset({ name: updatedOrg.name }); + } catch (err) { + toast.error(`Error: ${err.message}`); + } + }; + + return isViewer ? ( +

You are not authorized to perform this action.

+ ) : ( + +
+ ( + + Organization Name + + + + + + + )} + /> + + + +
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx index 35cca19590..28b57de899 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/members/page.tsx @@ -19,7 +19,7 @@ import { SettingsId } from "@formbricks/ui/SettingsId"; import { SettingsCard } from "../../components/SettingsCard"; import { DeleteOrganization } from "./components/DeleteOrganization"; import { EditMemberships } from "./components/EditMemberships"; -import { EditOrganizationName } from "./components/EditOrganizationName"; +import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm"; const MembersLoading = () => (
@@ -89,7 +89,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => { )} - ; export const ZUserUpdateInput = z.object({ - name: z.string().nullish(), + name: z.string().optional(), email: z.string().email().optional(), emailVerified: z.date().nullish(), onboardingCompleted: z.boolean().optional(), @@ -53,7 +56,10 @@ export const ZUserUpdateInput = z.object({ export type TUserUpdateInput = z.infer; export const ZUserCreateInput = z.object({ - name: z.string().optional(), + name: z + .string({ message: "Name is required" }) + .trim() + .min(1, { message: "Name should be at least 1 character long" }), email: z.string().email(), emailVerified: z.date().optional(), onboardingCompleted: z.boolean().optional(),