diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx index f9c864da0a..74d15b10cf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/page.tsx @@ -9,6 +9,9 @@ import { getEnvironment } from "@formbricks/lib/services/environment"; export default async function ProfileSettingsPage({ params }) { const environment = await getEnvironment(params.environmentId); + if (!environment) { + throw new Error("Environment not found"); + } return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx index acaa6fff34..093d950c81 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx @@ -2,7 +2,7 @@ import { cn } from "@formbricks/lib/cn"; import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; -import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product"; +import { TProduct } from "@formbricks/types/v1/product"; import { Button, ColorPicker, Label, Switch } from "@formbricks/ui"; import { useState } from "react"; import toast from "react-hot-toast"; @@ -20,10 +20,7 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { const handleUpdateHighlightBorder = async () => { try { setUpdatingBorder(true); - let inputProduct: Partial = { - highlightBorderColor: color, - }; - await updateProductAction(product.id, inputProduct); + await updateProductAction(product.id, { highlightBorderColor: color }); toast.success("Border color updated successfully."); } catch (error) { toast.error(`Error: ${error.message}`); @@ -46,54 +43,6 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { } }; - /* return ( -
-
-
- -

Show highlight border

-
- - {showHighlightBorder && color ? ( - <> - - - - ) : null} - - -
- -
-

Preview

-
-

How easy was it for you to do this?

-
- {[1, 2, 3, 4, 5].map((num) => ( -
- {num} -
- ))} -
-
-
-
- ); -}; */ - return (
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx new file mode 100644 index 0000000000..610ab9df07 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx @@ -0,0 +1,32 @@ +import { getProducts } from "@formbricks/lib/services/product"; +import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; +import { TProduct } from "@formbricks/types/v1/product"; +import DeleteProductRender from "@/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; + +type DeleteProductProps = { + environmentId: string; + product: TProduct; +}; + +export default async function DeleteProduct({ environmentId, product }: DeleteProductProps) { + const session = await getServerSession(authOptions); + const team = await getTeamByEnvironmentId(environmentId); + const availableProducts = team ? await getProducts(team.id) : null; + + const role = team ? session?.user.teams.find((foundTeam) => foundTeam.id === team.id)?.role : null; + const availableProductsLength = availableProducts ? availableProducts.length : 0; + const isUserAdminOrOwner = role === "admin" || role === "owner"; + const isDeleteDisabled = availableProductsLength <= 1 || !isUserAdminOrOwner; + + return ( + + ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx new file mode 100644 index 0000000000..25ae93cb6b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/settings/product/actions"; +import DeleteDialog from "@/components/shared/DeleteDialog"; +import { truncate } from "@/lib/utils"; +import { TProduct } from "@formbricks/types/v1/product"; +import { Button } from "@formbricks/ui"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import toast from "react-hot-toast"; + +type DeleteProductRenderProps = { + environmentId: string; + isDeleteDisabled: boolean; + isUserAdminOrOwner: boolean; + product: TProduct; + userId: string; +}; + +const DeleteProductRender: React.FC = ({ + environmentId, + isDeleteDisabled, + isUserAdminOrOwner, + product, + userId, +}) => { + const router = useRouter(); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const handleDeleteProduct = async () => { + try { + const deletedProduct = await deleteProductAction(environmentId, userId, product.id); + + if (!!deletedProduct?.id) { + toast.success("Product deleted successfully."); + router.push("/"); + } + } catch (err) { + toast.error("Could not delete product."); + setIsDeleteDialogOpen(false); + } + }; + + return ( +
+ {!isDeleteDisabled && ( +
+

+ Delete {truncate(product.name, 30)} +  incl. all surveys, responses, people, actions and attributes.{" "} + This action cannot be undone. +

+ +
+ )} + + {isDeleteDisabled && ( +

+ {!isUserAdminOrOwner + ? "Only Admin or Owners can delete products." + : "This is your only product, it cannot be deleted. Create a new product first."} +

+ )} + + +
+ ); +}; + +export default DeleteProductRender; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/EditProductName.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/EditProductName.tsx new file mode 100644 index 0000000000..420f444286 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/EditProductName.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { updateProductAction } from "./actions"; +import { TProduct } from "@formbricks/types/v1/product"; +import { useRouter } from "next/navigation"; +import { SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { Button, Input, Label } from "@formbricks/ui"; + +type TEditProductName = { + name: string; +}; + +type EditProductNameProps = { + product: TProduct; + environmentId: string; +}; + +const EditProductName: React.FC = ({ product, environmentId }) => { + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + name: product.name, + }, + }); + + const updateProduct: SubmitHandler = async (data) => { + try { + await updateProductAction(environmentId, product.id, data); + toast.success("Product name updated successfully."); + + router.refresh(); + } catch (err) { + toast.error(`Error: ${err.message}`); + } + }; + + return ( +
+ + + + {errors?.name ? ( +
+

{errors?.name?.message}

+
+ ) : null} + + +
+ ); +}; + +export default EditProductName; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/EditWaitingTime.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/EditWaitingTime.tsx new file mode 100644 index 0000000000..984f34c4a7 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/EditWaitingTime.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; + +import { Button, Input, Label } from "@formbricks/ui"; +import { TProduct } from "@formbricks/types/v1/product"; +import { updateProductAction } from "./actions"; + +type EditWaitingTimeFormValues = { + recontactDays: number; +}; + +type EditWaitingTimeProps = { + environmentId: string; + product: TProduct; +}; + +const EditWaitingTime: React.FC = ({ product, environmentId }) => { + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + recontactDays: product.recontactDays, + }, + }); + + const updateWaitingTime: SubmitHandler = async (data) => { + try { + await updateProductAction(environmentId, product.id, data); + toast.success("Waiting period updated successfully."); + + router.refresh(); + } catch (err) { + toast.error(`Error: ${err.message}`); + } + }; + + return ( +
+ + + + {errors?.recontactDays ? ( +
+

{errors?.recontactDays?.message}

+
+ ) : null} + + +
+ ); +}; + +export default EditWaitingTime; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts new file mode 100644 index 0000000000..d78b19bc46 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/actions.ts @@ -0,0 +1,84 @@ +"use server"; + +import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/services/product"; +import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product"; +import { getServerSession } from "next-auth"; +import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { getEnvironment } from "@formbricks/lib/services/environment"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; +import { getMembershipByUserId } from "@formbricks/lib/services/membership"; + +export const updateProductAction = async ( + environmentId: string, + productId: string, + data: Partial +): Promise => { + const session = await getServerSession(); + + if (!session?.user) { + throw new AuthenticationError("Not authenticated"); + } + + // get the environment from service and check if the user is allowed to update the product + let environment: TEnvironment | null = null; + + try { + environment = await getEnvironment(environmentId); + + if (!environment) { + throw new ResourceNotFoundError("Environment", "Environment not found"); + } + } catch (err) { + throw err; + } + + if (!hasUserEnvironmentAccess(session.user, environment.id)) { + throw new AuthenticationError("You don't have access to this environment"); + } + + const updatedProduct = await updateProduct(productId, data); + return updatedProduct; +}; + +export const deleteProductAction = async (environmentId: string, userId: string, productId: string) => { + const session = await getServerSession(); + + if (!session?.user) { + throw new AuthenticationError("Not authenticated"); + } + + // get the environment from service and check if the user is allowed to update the product + let environment: TEnvironment | null = null; + + try { + environment = await getEnvironment(environmentId); + + if (!environment) { + throw new ResourceNotFoundError("Environment", "Environment not found"); + } + } catch (err) { + throw err; + } + + if (!hasUserEnvironmentAccess(session.user, environment.id)) { + throw new AuthenticationError("You don't have access to this environment"); + } + + const team = await getTeamByEnvironmentId(environmentId); + const membership = team ? await getMembershipByUserId(userId, team.id) : null; + + if (membership?.role !== "admin" && membership?.role !== "owner") { + throw new AuthenticationError("You are not allowed to delete products."); + } + + const availableProducts = team ? await getProducts(team.id) : null; + + if (!!availableProducts && availableProducts?.length <= 1) { + throw new Error("You can't delete the last product in the environment."); + } + + const deletedProduct = await deleteProduct(productId); + return deletedProduct; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/editProduct.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/editProduct.tsx deleted file mode 100644 index 1c43680bb9..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/product/editProduct.tsx +++ /dev/null @@ -1,206 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useForm, useWatch } from "react-hook-form"; -import toast from "react-hot-toast"; -import { useRouter } from "next/navigation"; - -import DeleteDialog from "@/components/shared/DeleteDialog"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; - -import { deleteProduct, useProduct } from "@/lib/products/products"; -import { truncate } from "@/lib/utils"; - -import { useEnvironment } from "@/lib/environments/environments"; -import { useProductMutation } from "@/lib/products/mutateProducts"; -import { Button, ErrorComponent, Input, Label } from "@formbricks/ui"; -import { useProfile } from "@/lib/profile"; -import { useMembers } from "@/lib/members"; - -export function EditProductName({ environmentId }) { - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - const { isMutatingProduct, triggerProductMutate } = useProductMutation(environmentId); - const { mutateEnvironment } = useEnvironment(environmentId); - - const { register, handleSubmit, control, setValue } = useForm(); - - const productName = useWatch({ - control, - name: "name", - }); - const isProductNameInputEmpty = !productName?.trim(); - const currentProductName = productName?.trim().toLowerCase() ?? ""; - const previousProductName = product?.name?.trim().toLowerCase() ?? ""; - - useEffect(() => { - setValue("name", product?.name ?? ""); - }, [product?.name]); - - if (isLoadingProduct) { - return ; - } - if (isErrorProduct) { - return ; - } - - return ( -
{ - triggerProductMutate(data) - .then(() => { - toast.success("Product name updated successfully."); - mutateEnvironment(); - }) - .catch((error) => { - toast.error(`Error: ${error.message}`); - }); - })}> - - - - -
- ); -} - -export function EditWaitingTime({ environmentId }) { - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - const { isMutatingProduct, triggerProductMutate } = useProductMutation(environmentId); - - const { register, handleSubmit } = useForm(); - - if (isLoadingProduct) { - return ; - } - if (isErrorProduct) { - return ; - } - - return ( -
{ - triggerProductMutate(data) - .then(() => { - toast.success("Waiting period updated successfully."); - }) - .catch((error) => { - toast.error(`Error: ${error.message}`); - }); - })}> - - - - -
- ); -} - -export function DeleteProduct({ environmentId }) { - const router = useRouter(); - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [deletingProduct, setDeletingProduct] = useState(false); - - const { profile } = useProfile(); - const { team } = useMembers(environmentId); - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - const { environment } = useEnvironment(environmentId); - - const availableProducts = environment?.availableProducts?.length; - const role = team?.members?.filter((member) => member?.userId === profile?.id)[0]?.role; - const isUserAdminOrOwner = role === "admin" || role === "owner"; - const isDeleteDisabled = availableProducts <= 1 || !isUserAdminOrOwner; - - if (isLoadingProduct) { - return ; - } - if (isErrorProduct) { - return ; - } - - const handleDeleteProduct = async () => { - if (environment?.availableProducts?.length <= 1) { - toast.error("Cannot delete product. Your team needs at least 1."); - setIsDeleteDialogOpen(false); - return; - } - setDeletingProduct(true); - const deleteProductRes = await deleteProduct(environmentId); - setDeletingProduct(false); - - if (deleteProductRes?.id?.length > 0) { - toast.success("Product deleted successfully."); - router.push("/"); - } else if (deleteProductRes?.message?.length > 0) { - toast.error(deleteProductRes.message); - setIsDeleteDialogOpen(false); - } else { - toast.error("Error deleting product. Please try again."); - } - }; - - return ( -
- {!isDeleteDisabled && ( -
-

- Delete {truncate(product?.name, 30)} -  incl. all surveys, responses, people, actions and attributes.{" "} - This action cannot be undone. -

- -
- )} - {isDeleteDisabled && ( -

- {!isUserAdminOrOwner - ? "Only Admin or Owners can delete products." - : "This is your only product, it cannot be deleted. Create a new product first."} -

- )} - -
- ); -} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/loading.tsx new file mode 100644 index 0000000000..d4b116e749 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/loading.tsx @@ -0,0 +1,49 @@ +function LoadingCard({ title, description, skeletonLines }) { + return ( +
+
+

{title}

+

{description}

+
+
+
+ {skeletonLines.map((line, index) => ( +
+
+
+ ))} +
+
+
+ ); +} + +export default function Loading() { + const cards = [ + { + title: "Product Name", + description: "Change your products name.", + skeletonLines: [{ classes: "h-4 w-28" }, { classes: "h-6 w-64" }, { classes: "h-8 w-24" }], + }, + { + title: "Recontact Waiting Time", + description: "Control how frequently users can be surveyed across all surveys.", + skeletonLines: [{ classes: "h-4 w-28" }, { classes: "h-6 w-64" }, { classes: "h-8 w-24" }], + }, + { + title: "Delete Product", + description: + "Delete product with all surveys, responses, people, actions and attributes. This cannot be undone.", + skeletonLines: [{ classes: "h-4 w-96" }, { classes: "h-8 w-24" }], + }, + ]; + + return ( +
+

Product Settings

+ {cards.map((card, index) => ( + + ))} +
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/page.tsx index fd42db7896..0af1b64da5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/product/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/page.tsx @@ -1,23 +1,38 @@ +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; + import SettingsCard from "../SettingsCard"; import SettingsTitle from "../SettingsTitle"; -import { EditProductName, EditWaitingTime, DeleteProduct } from "./editProduct"; -export default function ProfileSettingsPage({ params }) { +import EditProductName from "./EditProductName"; +import EditWaitingTime from "./EditWaitingTime"; +import DeleteProduct from "./DeleteProduct"; +import { getEnvironment } from "@formbricks/lib/services/environment"; + +export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) { + const [, product] = await Promise.all([ + getEnvironment(params.environmentId), + getProductByEnvironmentId(params.environmentId), + ]); + + if (!product) { + throw new Error("Product not found"); + } + return (
- + - + - +
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx index ef99ad31cc..5058fbb62a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -19,6 +19,9 @@ export default async function SurveysList({ environmentId }: { environmentId: st } const environment = await getEnvironment(environmentId); + if (!environment) { + throw new Error("Environment not found"); + } const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId); const environments: TEnvironment[] = await getEnvironments(product.id); const otherEnvironment = environments.find((e) => e.type !== environment.type)!; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx index 395ee7d6fa..9e4c312f3d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx @@ -11,6 +11,10 @@ export default async function SurveyTemplatesPage({ params }) { throw new Error("Product not found"); } + if (!environment) { + throw new Error("Environment not found"); + } + return ( ); diff --git a/packages/lib/services/environment.ts b/packages/lib/services/environment.ts index 1f6c9d966f..0cb54da5e0 100644 --- a/packages/lib/services/environment.ts +++ b/packages/lib/services/environment.ts @@ -9,7 +9,7 @@ import { ZEnvironment, ZEnvironmentUpdateInput, ZId } from "@formbricks/types/v1 import { cache } from "react"; import { validateInputs } from "../utils/validate"; -export const getEnvironment = cache(async (environmentId: string): Promise => { +export const getEnvironment = cache(async (environmentId: string): Promise => { validateInputs([environmentId, ZId]); let environmentPrisma; try { @@ -19,9 +19,7 @@ export const getEnvironment = cache(async (environmentId: string): Promise => { + const membership = await prisma.membership.findUnique({ + where: { + userId_teamId: { + userId, + teamId, + }, + }, + }); + + if (!membership) return null; + + return membership; + } +); diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts index 4cc71b5f85..a3d726c4e6 100644 --- a/packages/lib/services/product.ts +++ b/packages/lib/services/product.ts @@ -49,6 +49,7 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr throw new ValidationError("EnvironmentId is required"); } let productPrisma; + try { productPrisma = await prisma.product.findFirst({ where: { @@ -90,7 +91,6 @@ export const updateProduct = async ( if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError("Database operation failed"); } - throw error; } try { const product = ZProduct.parse(updatedProduct); @@ -121,3 +121,13 @@ export const getProduct = cache(async (productId: string): Promise => { + const product = await prisma.product.delete({ + where: { + id: productId, + }, + }); + + return product; +}); diff --git a/packages/lib/services/profile.ts b/packages/lib/services/profile.ts index 691a5338a6..f00f7a58b6 100644 --- a/packages/lib/services/profile.ts +++ b/packages/lib/services/profile.ts @@ -1,14 +1,12 @@ import { prisma } from "@formbricks/database"; +import { ZId } from "@formbricks/types/v1/environment"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; -import { Prisma } from "@prisma/client"; -import { TProfile, ZProfileUpdateInput } from "@formbricks/types/v1/profile"; -import { deleteTeam } from "./team"; -import { MembershipRole } from "@prisma/client"; +import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/memberships"; +import { TProfile, TProfileUpdateInput, ZProfileUpdateInput } from "@formbricks/types/v1/profile"; +import { MembershipRole, Prisma } from "@prisma/client"; import { cache } from "react"; import { validateInputs } from "../utils/validate"; -import { ZId } from "@formbricks/types/v1/environment"; -import { TMembership, TMembershipRole, ZMembershipRole } from "@formbricks/types/v1/membership"; -import { TProfileUpdateInput } from "@formbricks/types/v1/profile"; +import { deleteTeam } from "./team"; const responseSelection = { id: true, @@ -59,7 +57,7 @@ const updateUserMembership = async (teamId: string, userId: string, role: TMembe }); }; -const getAdminMemberships = (memberships: TMembership[]) => +const getAdminMemberships = (memberships: TMembership[]): TMembership[] => memberships.filter((membership) => membership.role === MembershipRole.admin); // function to update a user's profile @@ -107,12 +105,7 @@ export const deleteProfile = async (personId: string): Promise => { select: { id: true, name: true, - memberships: { - select: { - userId: true, - role: true, - }, - }, + memberships: true, }, }, }, diff --git a/packages/types/v1/membership.ts b/packages/types/v1/memberships.ts similarity index 81% rename from packages/types/v1/membership.ts rename to packages/types/v1/memberships.ts index f735046875..467050fff0 100644 --- a/packages/types/v1/membership.ts +++ b/packages/types/v1/memberships.ts @@ -1,11 +1,14 @@ -import { z } from "zod"; +import z from "zod"; export const ZMembershipRole = z.enum(["owner", "admin", "editor", "developer", "viewer"]); +export type TMembershipRole = z.infer; + export const ZMembership = z.object({ - role: ZMembershipRole, + teamId: z.string(), userId: z.string(), + accepted: z.boolean(), + role: ZMembershipRole, }); export type TMembership = z.infer; -export type TMembershipRole = z.infer;