From 357628b0d6d1cf79ec496f11dc8320faefaa80cc Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Tue, 11 Apr 2023 10:42:33 +0200 Subject: [PATCH] Feature create new products (#220) * feat: add modal to add new product to current team * feat: add create product endpoint and helper function * feat: display available products in dropdown and include environmentId in request * new icon --------- Co-authored-by: moritzrengert --- .../[environmentId]/AddProductModal.tsx | 70 +++++++++++++++++++ .../[environmentId]/EnvironmentsNavbar.tsx | 31 ++++++-- apps/web/lib/products/products.ts | 12 ++++ .../v1/environments/[environmentId]/index.ts | 24 ++++++- .../[environmentId]/product/index.ts | 52 ++++++++++++++ 5 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/environments/[environmentId]/AddProductModal.tsx diff --git a/apps/web/app/environments/[environmentId]/AddProductModal.tsx b/apps/web/app/environments/[environmentId]/AddProductModal.tsx new file mode 100644 index 0000000000..0c1ab6b1da --- /dev/null +++ b/apps/web/app/environments/[environmentId]/AddProductModal.tsx @@ -0,0 +1,70 @@ +"use client"; + +import Modal from "@/components/shared/Modal"; +import { createProduct } from "@/lib/products/products"; +import { Button, Input, Label } from "@formbricks/ui"; +import { PlusCircleIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; + +interface AddProductModalProps { + environmentId: string; + open: boolean; + setOpen: (v: boolean) => void; +} + +export default function AddProductModal({ environmentId, open, setOpen }: AddProductModalProps) { + const router = useRouter(); + const { register, handleSubmit } = useForm(); + + const submitProduct = async (data) => { + const newEnv = await createProduct(environmentId, data); + router.push(`/environments/${newEnv.id}/`); + setOpen(false); + }; + + return ( + +
+
+
+
+
+ +
+
+
Add Product
+
Create a new product for your team.
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+ ); +} diff --git a/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx index e852bc3874..58751a08d6 100644 --- a/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx +++ b/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx @@ -46,6 +46,7 @@ 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; @@ -59,6 +60,8 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false); + const [showAddProductModal, setShowAddProductModal] = useState(false); + useEffect(() => { if (environment && environment.widgetSetupCompleted) { setWidgetSetupCompleted(true); @@ -171,6 +174,12 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme router.push(`/environments/${newEnvironmentId}/`); }; + const changeEnvironmentByProduct = (productId: string) => { + const product = environment.availableProducts.find((p) => p.id === productId); + const newEnvironmentId = product?.environments[0]?.id; + router.push(`/environments/${newEnvironmentId}/`); + }; + if (isLoadingEnvironment) { return ; } @@ -250,12 +259,21 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme - - {environment?.product?.name} - + + {environment?.availableProducts?.map((product) => ( + + {product.name} + + ))} + - + setShowAddProductModal(true)}> Add product @@ -327,6 +345,11 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme + setShowAddProductModal(val)} + environmentId={environmentId} + /> ); } diff --git a/apps/web/lib/products/products.ts b/apps/web/lib/products/products.ts index 9267c4936b..8ddf162fd3 100644 --- a/apps/web/lib/products/products.ts +++ b/apps/web/lib/products/products.ts @@ -15,3 +15,15 @@ export const useProduct = (environmentId: string) => { mutateProduct: mutate, }; }; + +export const createProduct = async (environmentId, product: { name: string }) => { + const response = await fetch(`/api/v1/environments/${environmentId}/product`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(product), + }); + + return response.json(); +}; diff --git a/apps/web/pages/api/v1/environments/[environmentId]/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/index.ts index 5c8d032f5d..63934c26a2 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/index.ts @@ -19,6 +19,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) include: { product: { select: { + id: true, name: true, teamId: true, brandColor: true, @@ -27,10 +28,31 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }, }, }); + if (environment === null) { return res.status(404).json({ message: "This environment doesn't exist" }); } - return res.json(environment); + + const products = await prisma.product.findMany({ + where: { + teamId: environment.product.teamId, + }, + select: { + id: true, + name: true, + brandColor: true, + environments: { + where: { + type: "production", + }, + select: { + id: true, + }, + }, + }, + }); + + return res.json({ ...environment, availableProducts: products }); } if (req.method === "PUT") { diff --git a/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts index 97274e3ef9..47a83b3a0a 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts @@ -1,5 +1,7 @@ import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; +import { EnvironmentType } from "@prisma/client"; +import { populateEnvironment } from "@/lib/populate"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -63,6 +65,56 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) return res.json(prismaRes); } + // POST + else if (req.method === "POST") { + const { name } = req.body; + + // Get the teamId of the current environment + const environment = await prisma.environment.findUnique({ + where: { id: environmentId }, + select: { + product: { + select: { + teamId: true, + }, + }, + }, + }); + + if (!environment) { + res.status(404).json({ error: "Environment not found" }); + return; + } + + // Create a new product and associate it with the current team + const newProduct = await prisma.product.create({ + data: { + name, + team: { + connect: { id: environment.product.teamId }, + }, + environments: { + create: [ + { + type: EnvironmentType.production, + ...populateEnvironment, + }, + { + type: EnvironmentType.development, + ...populateEnvironment, + }, + ], + }, + }, + select: { + environments: true, + }, + }); + + const firstEnvironment = newProduct.environments[0]; + res.json(firstEnvironment); + } + // Unknown HTTP Method else { throw new Error(`The HTTP ${req.method} method is not supported by this route.`);