mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-20 03:00:57 -06:00
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 <moritz@rengert.de>
This commit is contained in:
@@ -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 (
|
||||
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-10 w-10 text-slate-500">
|
||||
<PlusCircleIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Add Product</div>
|
||||
<div className="text-sm text-slate-500">Create a new product for your team.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(submitProduct)}>
|
||||
<div className="flex w-full justify-between space-y-4 rounded-lg p-6">
|
||||
<div className="grid w-full gap-x-2">
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<Input placeholder="e.g. My New Product" {...register("name", { required: true })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Add product
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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 <LoadingSpinner />;
|
||||
}
|
||||
@@ -250,12 +259,21 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem>
|
||||
<span>{environment?.product?.name}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuRadioGroup
|
||||
value={environment?.product.id}
|
||||
onValueChange={changeEnvironmentByProduct}>
|
||||
{environment?.availableProducts?.map((product) => (
|
||||
<DropdownMenuRadioItem
|
||||
value={product.id}
|
||||
className="cursor-pointer"
|
||||
key={product.id}>
|
||||
{product.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem disabled>
|
||||
<DropdownMenuItem onClick={() => setShowAddProductModal(true)}>
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
<span>Add product</span>
|
||||
</DropdownMenuItem>
|
||||
@@ -327,6 +345,11 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AddProductModal
|
||||
open={showAddProductModal}
|
||||
setOpen={(val) => setShowAddProductModal(val)}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
Reference in New Issue
Block a user