diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml index 586ebb3edd..21bdb0a44e 100644 --- a/.github/workflows/nextjs-bundle-analysis.yml +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -39,6 +39,9 @@ jobs: version: 7 run_install: true + - name: create .env + run: cp .env.example .env + - name: Restore next build uses: actions/cache@v3 id: restore-build-cache diff --git a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx index d095f89151..701c29c1de 100644 --- a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx @@ -1,502 +1,46 @@ -"use client"; +export const revalidate = REVALIDATION_INTERVAL; -import FaveIcon from "@/app/favicon.ico"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from "@/components/shared/DropdownMenu"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import CreateTeamModal from "@/components/team/CreateTeamModal"; -import { - changeEnvironment, - changeEnvironmentByProduct, - changeEnvironmentByTeam, -} from "@/lib/environments/changeEnvironments"; -import { useEnvironment } from "@/lib/environments/environments"; -import { formbricksLogout } from "@/lib/formbricks"; -import { useMemberships } from "@/lib/memberships"; -import { useTeam } from "@/lib/teams/teams"; -import { capitalizeFirstLetter, truncate } from "@/lib/utils"; -import formbricks from "@formbricks/js"; -import { cn } from "@formbricks/lib/cn"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { - CustomersIcon, - DashboardIcon, - ErrorComponent, - FilterIcon, - FormIcon, - Popover, - PopoverContent, - PopoverTrigger, - ProfileAvatar, - SettingsIcon, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@formbricks/ui"; -import { - AdjustmentsVerticalIcon, - ArrowRightOnRectangleIcon, - ChatBubbleBottomCenterTextIcon, - ChevronDownIcon, - CodeBracketIcon, - CreditCardIcon, - DocumentCheckIcon, - HeartIcon, - PaintBrushIcon, - PlusIcon, - UserCircleIcon, - UsersIcon, -} from "@heroicons/react/24/solid"; -import clsx from "clsx"; -import { MenuIcon } from "lucide-react"; +import Navigation from "@/app/(app)/environments/[environmentId]/Navigation"; +import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment"; +import { getProducts } from "@formbricks/lib/services/product"; +import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team"; +import { ErrorComponent } from "@formbricks/ui"; import type { Session } from "next-auth"; -import { signOut } from "next-auth/react"; -import Image from "next/image"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; -import AddProductModal from "./AddProductModal"; interface EnvironmentsNavbarProps { environmentId: string; session: Session; } -export default function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) { - const router = useRouter(); - const pathname = usePathname(); +export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) { + const [environment, teams, team] = await Promise.all([ + getEnvironment(environmentId), + getTeamsByUserId(session.user.id), + getTeamByEnvironmentId(environmentId), + ]); - const { environment, isErrorEnvironment, isLoadingEnvironment } = useEnvironment(environmentId); - const { memberships, isErrorMemberships, isLoadingMemberships } = useMemberships(); - const { team } = useTeam(environmentId); - - const [currentTeamName, setCurrentTeamName] = useState(""); - const [currentTeamId, setCurrentTeamId] = useState(""); - const [loading, setLoading] = useState(false); - const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false); - const [showAddProductModal, setShowAddProductModal] = useState(false); - const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); - - const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false); - - useEffect(() => { - if (environment && environment.widgetSetupCompleted) { - setWidgetSetupCompleted(true); - } else { - setWidgetSetupCompleted(false); - } - }, [environment]); - - useEffect(() => { - if (team && team.name !== "") { - setCurrentTeamName(team.name); - setCurrentTeamId(team.id); - } - }, [team]); - - const navigation = useMemo( - () => [ - { - name: "Surveys", - href: `/environments/${environmentId}/surveys`, - icon: FormIcon, - current: pathname?.includes("/surveys"), - }, - { - name: "People", - href: `/environments/${environmentId}/people`, - icon: CustomersIcon, - current: pathname?.includes("/people"), - }, - { - name: "Actions & Attributes", - href: `/environments/${environmentId}/actions`, - icon: FilterIcon, - current: pathname?.includes("/actions") || pathname?.includes("/attributes"), - }, - { - name: "Integrations", - href: `/environments/${environmentId}/integrations`, - icon: DashboardIcon, - current: pathname?.includes("/integrations"), - }, - { - name: "Settings", - href: `/environments/${environmentId}/settings/profile`, - icon: SettingsIcon, - current: pathname?.includes("/settings"), - }, - ], - [environmentId, pathname] - ); - - const dropdownnavigation = [ - { - title: "Survey", - links: [ - { - icon: AdjustmentsVerticalIcon, - label: "Product Settings", - href: `/environments/${environmentId}/settings/product`, - }, - { - icon: PaintBrushIcon, - label: "Look & Feel", - href: `/environments/${environmentId}/settings/lookandfeel`, - }, - ], - }, - { - title: "Account", - links: [ - { - icon: UserCircleIcon, - label: "Profile", - href: `/environments/${environmentId}/settings/profile`, - }, - { icon: UsersIcon, label: "Team", href: `/environments/${environmentId}/settings/members` }, - { - icon: CreditCardIcon, - label: "Billing & Plan", - href: `/environments/${environmentId}/settings/billing`, - hidden: !IS_FORMBRICKS_CLOUD, - }, - ], - }, - { - title: "Setup", - links: [ - { - icon: DocumentCheckIcon, - label: "Setup checklist", - href: `/environments/${environmentId}/settings/setup`, - hidden: widgetSetupCompleted, - }, - { - icon: CodeBracketIcon, - label: "Developer Docs", - href: "https://formbricks.com/docs", - target: "_blank", - }, - { - icon: HeartIcon, - label: "Contribute to Formbricks", - href: "https://github.com/formbricks/formbricks", - target: "_blank", - }, - ], - }, - ]; - - const handleEnvironmentChange = (environmentType: "production" | "development") => { - changeEnvironment(environmentType, environment, router); - }; - - const handleEnvironmentChangeByProduct = (productId: string) => { - changeEnvironmentByProduct(productId, environment, router); - }; - - const handleEnvironmentChangeByTeam = (teamId: string) => { - changeEnvironmentByTeam(teamId, memberships, router); - }; - - if (isLoadingEnvironment || loading || isLoadingMemberships) { - return ; - } - - if (isErrorEnvironment || isErrorMemberships || !environment || !memberships) { + if (!team || !environment) { return ; } - if (pathname?.includes("/edit")) return null; + const [products, environments] = await Promise.all([ + getProducts(team.id), + getEnvironments(environment.productId), + ]); + + if (!products || !environments || !teams) { + return ; + } return ( - + ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx b/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx new file mode 100644 index 0000000000..6f9264d56e --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/NavbarLoading.tsx @@ -0,0 +1,20 @@ +export default function NavbarLoading() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx new file mode 100644 index 0000000000..c2790c8e35 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx @@ -0,0 +1,495 @@ +"use client"; + +import FaveIcon from "@/app/favicon.ico"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/shared/DropdownMenu"; +import CreateTeamModal from "@/components/team/CreateTeamModal"; +import { formbricksLogout } from "@/lib/formbricks"; +import { capitalizeFirstLetter, truncate } from "@/lib/utils"; +import formbricks from "@formbricks/js"; +import { cn } from "@formbricks/lib/cn"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TTeam } from "@formbricks/types/v1/teams"; +import { + CustomersIcon, + DashboardIcon, + FilterIcon, + FormIcon, + Popover, + PopoverContent, + PopoverTrigger, + ProfileAvatar, + SettingsIcon, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@formbricks/ui"; +import { + AdjustmentsVerticalIcon, + ArrowRightOnRectangleIcon, + ChatBubbleBottomCenterTextIcon, + ChevronDownIcon, + CodeBracketIcon, + CreditCardIcon, + DocumentCheckIcon, + HeartIcon, + PaintBrushIcon, + PlusIcon, + UserCircleIcon, + UsersIcon, +} from "@heroicons/react/24/solid"; +import clsx from "clsx"; +import { MenuIcon } from "lucide-react"; +import type { Session } from "next-auth"; +import { signOut } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import AddProductModal from "./AddProductModal"; + +interface NavigationProps { + environment: TEnvironment; + teams: TTeam[]; + session: Session; + team: TTeam; + products: TProduct[]; + environments: TEnvironment[]; +} + +export default function Navigation({ + environment, + teams, + team, + session, + products, + environments, +}: NavigationProps) { + const router = useRouter(); + const pathname = usePathname(); + const [currentTeamName, setCurrentTeamName] = useState(""); + const [currentTeamId, setCurrentTeamId] = useState(""); + const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false); + const [showAddProductModal, setShowAddProductModal] = useState(false); + const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); + const product = products.find((product) => product.id === environment.productId); + const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false); + + useEffect(() => { + if (environment && environment.widgetSetupCompleted) { + setWidgetSetupCompleted(true); + } else { + setWidgetSetupCompleted(false); + } + }, [environment]); + + useEffect(() => { + if (team && team.name !== "") { + setCurrentTeamName(team.name); + setCurrentTeamId(team.id); + } + }, [team]); + + const navigation = useMemo( + () => [ + { + name: "Surveys", + href: `/environments/${environment.id}/surveys`, + icon: FormIcon, + current: pathname?.includes("/surveys"), + }, + { + name: "People", + href: `/environments/${environment.id}/people`, + icon: CustomersIcon, + current: pathname?.includes("/people"), + }, + { + name: "Actions & Attributes", + href: `/environments/${environment.id}/actions`, + icon: FilterIcon, + current: pathname?.includes("/actions") || pathname?.includes("/attributes"), + }, + { + name: "Integrations", + href: `/environments/${environment.id}/integrations`, + icon: DashboardIcon, + current: pathname?.includes("/integrations"), + }, + { + name: "Settings", + href: `/environments/${environment.id}/settings/profile`, + icon: SettingsIcon, + current: pathname?.includes("/settings"), + }, + ], + [environment.id, pathname] + ); + + const dropdownnavigation = [ + { + title: "Survey", + links: [ + { + icon: AdjustmentsVerticalIcon, + label: "Product Settings", + href: `/environments/${environment.id}/settings/product`, + }, + { + icon: PaintBrushIcon, + label: "Look & Feel", + href: `/environments/${environment.id}/settings/lookandfeel`, + }, + ], + }, + { + title: "Account", + links: [ + { + icon: UserCircleIcon, + label: "Profile", + href: `/environments/${environment.id}/settings/profile`, + }, + { icon: UsersIcon, label: "Team", href: `/environments/${environment.id}/settings/members` }, + { + icon: CreditCardIcon, + label: "Billing & Plan", + href: `/environments/${environment.id}/settings/billing`, + hidden: !IS_FORMBRICKS_CLOUD, + }, + ], + }, + { + title: "Setup", + links: [ + { + icon: DocumentCheckIcon, + label: "Setup checklist", + href: `/environments/${environment.id}/settings/setup`, + hidden: widgetSetupCompleted, + }, + { + icon: CodeBracketIcon, + label: "Developer Docs", + href: "https://formbricks.com/docs", + target: "_blank", + }, + { + icon: HeartIcon, + label: "Contribute to Formbricks", + href: "https://github.com/formbricks/formbricks", + target: "_blank", + }, + ], + }, + ]; + + const handleEnvironmentChange = (environmentType: "production" | "development") => { + const newEnvironmentId = environments.find((e) => e.type === environmentType)?.id; + if (newEnvironmentId) { + router.push(`/environments/${newEnvironmentId}/`); + } + }; + + const handleEnvironmentChangeByProduct = (productId: string) => { + router.push(`/products/${productId}/`); + }; + + const handleEnvironmentChangeByTeam = (teamId: string) => { + router.push(`/teams/${teamId}/`); + }; + + if (pathname?.includes("/edit")) return null; + + return ( + <> + {product && ( + + )} + + ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx index 9a11e48e91..1ad90e1cd5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx @@ -20,6 +20,10 @@ export default async function ApiKeyList({ }; const product = await getProductByEnvironmentId(environmentId); + if (!product) { + throw new Error("Product not found"); + } + const environments = await getEnvironments(product.id); const environmentTypeId = findEnvironmentByType(environments, environmentType); const apiKeys = await getApiKeys(environmentTypeId); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx index 00042b4512..f1ce4391ef 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx @@ -11,6 +11,9 @@ import { EditHighlightBorder } from "./EditHighlightBorder"; export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) { const product = await 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 f82db0b540..64147ef5cb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/SurveyList.tsx @@ -14,6 +14,10 @@ import { SURVEY_BASE_URL } from "@formbricks/lib/constants"; export default async function SurveysList({ environmentId }: { environmentId: string }) { const product = await getProductByEnvironmentId(environmentId); + if (!product) { + throw new Error("Product not found"); + } + const environment = await getEnvironment(environmentId); const surveys: TSurveyWithAnalytics[] = await getSurveysWithAnalytics(environmentId); const environments: TEnvironment[] = await getEnvironments(product.id); 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 f580ef22c0..395ee7d6fa 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/page.tsx @@ -7,6 +7,10 @@ export default async function SurveyTemplatesPage({ params }) { const environment = await getEnvironment(environmentId); const product = await getProductByEnvironmentId(environmentId); + if (!product) { + throw new Error("Product not found"); + } + return ( ); diff --git a/apps/web/app/(redirects)/products/[productId]/route.ts b/apps/web/app/(redirects)/products/[productId]/route.ts new file mode 100644 index 0000000000..1ff1bc6506 --- /dev/null +++ b/apps/web/app/(redirects)/products/[productId]/route.ts @@ -0,0 +1,24 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { hasTeamAccess } from "@/lib/api/apiHelper"; +import { getEnvironments } from "@formbricks/lib/services/environment"; +import { getProduct } from "@formbricks/lib/services/product"; +import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors"; +import { getServerSession } from "next-auth"; +import { notFound, redirect } from "next/navigation"; + +export async function GET(_: Request, context: { params: { productId: string } }) { + const productId = context?.params?.productId; + if (!productId) return notFound(); + // check auth + const session = await getServerSession(authOptions); + if (!session) throw new AuthenticationError("Not authenticated"); + const product = await getProduct(productId); + if (!product) return notFound(); + const hasAccess = await hasTeamAccess(session.user, product.teamId); + if (!hasAccess) throw new AuthorizationError("Unauthorized"); + // redirect to product's production environment + const environments = await getEnvironments(product.id); + const prodEnvironment = environments.find((e) => e.type === "production"); + if (!prodEnvironment) return notFound(); + redirect(`/environments/${prodEnvironment.id}/`); +} diff --git a/apps/web/app/(redirects)/teams/[teamId]/route.ts b/apps/web/app/(redirects)/teams/[teamId]/route.ts new file mode 100644 index 0000000000..7b67901dfe --- /dev/null +++ b/apps/web/app/(redirects)/teams/[teamId]/route.ts @@ -0,0 +1,26 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { hasTeamAccess } from "@/lib/api/apiHelper"; +import { getEnvironments } from "@formbricks/lib/services/environment"; +import { getProducts } from "@formbricks/lib/services/product"; +import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { notFound } from "next/navigation"; + +export async function GET(_: Request, context: { params: { teamId: string } }) { + const teamId = context?.params?.teamId; + if (!teamId) return notFound(); + // check auth + const session = await getServerSession(authOptions); + if (!session) throw new AuthenticationError("Not authenticated"); + const hasAccess = await hasTeamAccess(session.user, teamId); + if (!hasAccess) throw new AuthorizationError("Unauthorized"); + // redirect to first product's production environment + const products = await getProducts(teamId); + if (products.length === 0) return notFound(); + const firstProduct = products[0]; + const environments = await getEnvironments(firstProduct.id); + const prodEnvironment = environments.find((e) => e.type === "production"); + if (!prodEnvironment) return notFound(); + redirect(`/environments/${prodEnvironment.id}/`); +} diff --git a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts index cd906c943d..53cafea05c 100644 --- a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts +++ b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts @@ -109,6 +109,10 @@ export async function POST(req: Request, { params }): Promise { getProductByEnvironmentId(environmentId), ]); + if (!product) { + return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true); + } + // return state const state: TJsState = { person, diff --git a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts index e5889750a0..7f3a373fa4 100644 --- a/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts +++ b/apps/web/app/api/v1/js/people/[personId]/set-user-id/route.ts @@ -101,6 +101,10 @@ export async function POST(req: Request, { params }): Promise { getProductByEnvironmentId(environmentId), ]); + if (!product) { + return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true); + } + // return state const state: TJsState = { person, diff --git a/apps/web/app/api/v1/js/sync/route.ts b/apps/web/app/api/v1/js/sync/route.ts index c5321611dc..d19ad196f9 100644 --- a/apps/web/app/api/v1/js/sync/route.ts +++ b/apps/web/app/api/v1/js/sync/route.ts @@ -60,6 +60,10 @@ export async function POST(req: Request): Promise { captureNewSessionTelemetry(inputValidation.data.jsVersion); + if (!product) { + return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true); + } + // return state const state: TJsState = { person, @@ -87,6 +91,10 @@ export async function POST(req: Request): Promise { getProductByEnvironmentId(environmentId), ]); + if (!product) { + return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true); + } + captureNewSessionTelemetry(inputValidation.data.jsVersion); // return state @@ -144,6 +152,10 @@ export async function POST(req: Request): Promise { getProductByEnvironmentId(environmentId), ]); + if (!product) { + return responses.notFoundResponse("ProductByEnvironmentId", environmentId, true); + } + // return state const state: TJsState = { person, diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 7058cf57fc..0874c50315 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -36,6 +36,10 @@ export default async function LinkSurveyPage({ params, searchParams }) { // get product and person const product = await getProductByEnvironmentId(survey.environmentId); + if (!product) { + throw new Error("Product not found"); + } + const userId = searchParams.userId; let person; if (userId) { diff --git a/apps/web/components/team/CreateTeamModal.tsx b/apps/web/components/team/CreateTeamModal.tsx index 8a180b7a1c..750f5c6eeb 100644 --- a/apps/web/components/team/CreateTeamModal.tsx +++ b/apps/web/components/team/CreateTeamModal.tsx @@ -1,6 +1,5 @@ +import { createTeam } from "@/app/(app)/environments/[environmentId]/actions"; import Modal from "@/components/shared/Modal"; -import { changeEnvironmentByTeam } from "@/lib/environments/changeEnvironments"; -import { useMemberships } from "@/lib/memberships"; import { useProfile } from "@/lib/profile"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; @@ -8,7 +7,6 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; -import { createTeam } from "@/app/(app)/environments/[environmentId]/actions"; interface CreateTeamModalProps { open: boolean; @@ -18,7 +16,6 @@ interface CreateTeamModalProps { export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps) { const router = useRouter(); const { profile } = useProfile(); - const { mutateMemberships } = useMemberships(); const [loading, setLoading] = useState(false); const { register, handleSubmit } = useForm(); @@ -26,9 +23,8 @@ export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps) setLoading(true); const newTeam = await createTeam(data.name, (profile as any).id); - const newMemberships = await mutateMemberships(); - changeEnvironmentByTeam(newTeam.id, newMemberships, router); toast.success("Team created successfully!"); + router.push(`/teams/${newTeam.id}`); setOpen(false); setLoading(false); }; diff --git a/apps/web/env.mjs b/apps/web/env.mjs index d9dd88f2a3..e44fb676be 100644 --- a/apps/web/env.mjs +++ b/apps/web/env.mjs @@ -7,7 +7,7 @@ export const env = createEnv({ * Will throw if you access these variables on the client. */ server: { - WEBAPP_URL: z.string().url(), + WEBAPP_URL: z.string().url().optional(), DATABASE_URL: z.string().url(), PRISMA_GENERATE_DATAPROXY: z.enum(["true", ""]).optional(), NEXTAUTH_SECRET: z.string().min(1), diff --git a/apps/web/lib/environments/changeEnvironments.ts b/apps/web/lib/environments/changeEnvironments.ts deleted file mode 100644 index c4ca218dc9..0000000000 --- a/apps/web/lib/environments/changeEnvironments.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const changeEnvironment = (environmentType: string, environment: any, router: any) => { - const newEnvironmentId = environment.product.environments.find((e) => e.type === environmentType)?.id; - if (newEnvironmentId) { - router.push(`/environments/${newEnvironmentId}/`); - } -}; - -export const changeEnvironmentByProduct = (productId: string, environment: any, router: any) => { - const product = environment.availableProducts.find((p) => p.id === productId); - const newEnvironmentId = product?.environments[0]?.id; - if (newEnvironmentId) { - router.push(`/environments/${newEnvironmentId}/`); - } -}; - -export const changeEnvironmentByTeam = (teamId: string, memberships: any, router: any) => { - const newTeamMembership = memberships.find((m) => m.teamId === teamId); - const newTeamProduct = newTeamMembership?.team?.products?.[0]; - - if (newTeamProduct) { - const newEnvironmentId = newTeamProduct.environments.find((e) => e.type === "production")?.id; - - if (newEnvironmentId) { - router.push(`/environments/${newEnvironmentId}/`); - } - } -}; diff --git a/packages/lib/services/product.ts b/packages/lib/services/product.ts index 8a1f5f378d..cdd708f4db 100644 --- a/packages/lib/services/product.ts +++ b/packages/lib/services/product.ts @@ -1,11 +1,11 @@ -import "server-only"; import { prisma } from "@formbricks/database"; -import { z } from "zod"; -import { Prisma } from "@prisma/client"; -import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors"; -import { ZProduct } from "@formbricks/types/v1/product"; +import { DatabaseError, ValidationError } from "@formbricks/types/v1/errors"; import type { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product"; +import { ZProduct } from "@formbricks/types/v1/product"; +import { Prisma } from "@prisma/client"; import { cache } from "react"; +import "server-only"; +import { z } from "zod"; const selectProduct = { id: true, @@ -22,7 +22,26 @@ const selectProduct = { darkOverlay: true, }; -export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => { +export const getProducts = cache(async (teamId: string): Promise => { + try { + const products = await prisma.product.findMany({ + where: { + teamId, + }, + select: selectProduct, + }); + + return products; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } +}); + +export const getProductByEnvironmentId = cache(async (environmentId: string): Promise => { if (!environmentId) { throw new ValidationError("EnvironmentId is required"); } @@ -39,25 +58,13 @@ export const getProductByEnvironmentId = cache(async (environmentId: string): Pr select: selectProduct, }); - if (!productPrisma) { - throw new ResourceNotFoundError("Product for Environment", environmentId); - } + return productPrisma; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError("Database operation failed"); } throw error; } - - try { - const product = ZProduct.parse(productPrisma); - return product; - } catch (error) { - if (error instanceof z.ZodError) { - console.error(JSON.stringify(error.errors, null, 2)); - } - throw new ValidationError("Data validation of product failed"); - } }); export const updateProduct = async ( @@ -91,3 +98,22 @@ export const updateProduct = async ( throw new ValidationError("Data validation of product failed"); } }; + +export const getProduct = cache(async (productId: string): Promise => { + let productPrisma; + try { + productPrisma = await prisma.product.findUnique({ + where: { + id: productId, + }, + select: selectProduct, + }); + + return productPrisma; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + throw error; + } +}); diff --git a/packages/lib/services/team.ts b/packages/lib/services/team.ts index db213c2309..018d05b6ca 100644 --- a/packages/lib/services/team.ts +++ b/packages/lib/services/team.ts @@ -32,6 +32,29 @@ export const select = { stripeCustomerId: true, }; +export const getTeamsByUserId = cache(async (userId: string): Promise => { + try { + const teams = await prisma.team.findMany({ + where: { + memberships: { + some: { + userId, + }, + }, + }, + select, + }); + + return teams; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } +}); + export const getTeamByEnvironmentId = cache(async (environmentId: string): Promise => { try { const team = await prisma.team.findFirst({