diff --git a/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx index 9b2cf63cf9..695572808c 100644 --- a/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx +++ b/apps/web/app/environments/[environmentId]/EnvironmentsNavbar.tsx @@ -280,7 +280,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme Signed in as - {session.user.name.length > 30 ? ( + {session?.user?.name.length > 30 ? ( diff --git a/apps/web/app/environments/[environmentId]/people/[personId]/PersonDetails.tsx b/apps/web/app/environments/[environmentId]/people/[personId]/PersonDetails.tsx index eefa74c1f1..106a4c2c45 100644 --- a/apps/web/app/environments/[environmentId]/people/[personId]/PersonDetails.tsx +++ b/apps/web/app/environments/[environmentId]/people/[personId]/PersonDetails.tsx @@ -81,7 +81,7 @@ export default function PersonDetails({ environmentId, personId }: PersonDetails onClick={() => { setDeleteDialogOpen(true); }}> - + diff --git a/apps/web/app/environments/[environmentId]/settings/product/editProduct.tsx b/apps/web/app/environments/[environmentId]/settings/product/editProduct.tsx index 2ad6161cdd..7f91340f68 100644 --- a/apps/web/app/environments/[environmentId]/settings/product/editProduct.tsx +++ b/apps/web/app/environments/[environmentId]/settings/product/editProduct.tsx @@ -1,12 +1,21 @@ "use client"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useEnvironment } from "@/lib/environments/environments"; -import { useProductMutation } from "@/lib/products/mutateProducts"; -import { useProduct } from "@/lib/products/products"; -import { Button, ErrorComponent, Input, Label } from "@formbricks/ui"; +import { useState } from "react"; import { useForm } 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); @@ -88,3 +97,73 @@ export function EditWaitingTime({ environmentId }) { ); } + +export function DeleteProduct({ environmentId }) { + const router = useRouter(); + + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = 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. Environment needs at least 1."); + setIsDeleteDialogOpen(false); + return; + } + const deleteResponse = await deleteProduct(environmentId); + + if (deleteResponse?.message?.length > 0) { + toast.error(deleteResponse.message); + setIsDeleteDialogOpen(false); + } + if (deleteResponse?.id?.length > 0) { + toast.success("Product deleted successfully."); + router.push("/environments"); + } + } + + return ( +
+

+ Here you can 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.' + : 'Environment needs at least 1 product.' + } +

+ )} + +
+ ); +} diff --git a/apps/web/app/environments/[environmentId]/settings/product/page.tsx b/apps/web/app/environments/[environmentId]/settings/product/page.tsx index d42cdcd3bd..faffb0d79e 100644 --- a/apps/web/app/environments/[environmentId]/settings/product/page.tsx +++ b/apps/web/app/environments/[environmentId]/settings/product/page.tsx @@ -1,6 +1,6 @@ import SettingsCard from "../SettingsCard"; import SettingsTitle from "../SettingsTitle"; -import { EditProductName, EditWaitingTime } from "./editProduct"; +import { EditProductName, EditWaitingTime, DeleteProduct } from "./editProduct"; export default function ProfileSettingsPage({ params }) { return ( @@ -14,6 +14,11 @@ export default function ProfileSettingsPage({ params }) { description="Control how frequently users can be surveyed across all surveys."> + + + ); } diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx index c201b66bed..343402a7e9 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/ResponseTimeline.tsx @@ -68,7 +68,7 @@ export default function ResponseTimeline({ environmentId, surveyId }) {
{matchQandA.map((updatedResponse) => { return ( - + ); })}
diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx index 89858da5c4..1d3fc4a54d 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/SingleResponse.tsx @@ -1,13 +1,19 @@ +import DeleteDialog from "@/components/shared/DeleteDialog"; import { timeSince } from "@formbricks/lib/time"; import { PersonAvatar } from "@formbricks/ui"; import { CheckCircleIcon } from "@heroicons/react/24/solid"; +import { TrashIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; +import { useState } from "react"; +import toast from "react-hot-toast"; import { RatingResponse } from "../RatingResponse"; +import { deleteSubmission, useResponses } from "@/lib/responses/responses"; interface OpenTextSummaryProps { data: { id: string; personId: string; + surveyId: string, person: { id: string; createdAt: string; @@ -28,6 +34,7 @@ interface OpenTextSummaryProps { }[]; }; environmentId: string; + surveyId: string; } function findEmail(person) { @@ -35,9 +42,19 @@ function findEmail(person) { return emailAttribute ? emailAttribute.value : null; } -export default function SingleResponse({ data, environmentId }: OpenTextSummaryProps) { +export default function SingleResponse({ data, environmentId, surveyId }: OpenTextSummaryProps) { const email = data.person && findEmail(data.person); const displayIdentifier = email || data.personId; + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { mutateResponses } = useResponses(environmentId, surveyId) + + const handleDeleteSubmission = async () => { + const deleteResponse = await deleteSubmission(environmentId, data?.surveyId, data?.id); + mutateResponses(); + if(deleteResponse?.id?.length > 0) + toast.success("Submission deleted successfully."); + setDeleteDialogOpen(false); + }; console.log(data); @@ -67,6 +84,14 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP Completed )} +
+ +
@@ -91,6 +116,12 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP ))} + ); } diff --git a/apps/web/lib/products/products.ts b/apps/web/lib/products/products.ts index 3369d46f32..a1d3e340d4 100644 --- a/apps/web/lib/products/products.ts +++ b/apps/web/lib/products/products.ts @@ -16,7 +16,7 @@ export const useProduct = (environmentId: string) => { }; }; -export const createProduct = async (environmentId, product: { name: string }) => { +export const createProduct = async (environmentId: string, product: { name: string }) => { const response = await fetch(`/api/v1/environments/${environmentId}/product`, { method: "POST", headers: { @@ -27,3 +27,11 @@ export const createProduct = async (environmentId, product: { name: string }) => return response.json(); }; + +export const deleteProduct = async (environmentId: string) => { + const response = await fetch(`/api/v1/environments/${environmentId}/product`, { + method: "DELETE", + }); + + return response.json(); +}; diff --git a/apps/web/lib/responses/responses.ts b/apps/web/lib/responses/responses.ts index b5313aab99..5ecef7c783 100644 --- a/apps/web/lib/responses/responses.ts +++ b/apps/web/lib/responses/responses.ts @@ -11,6 +11,14 @@ export const useResponses = (environmentId: string, surveyId: string) => { responsesData: data, isLoadingResponses: isLoading, isErrorResponses: error, - mutateRespones: mutate, + mutateResponses: mutate, }; }; + +export const deleteSubmission = async (environmentId: string, surveyId: string, responseId: string) => { + const response = await fetch(`/api/v1/environments/${environmentId}/surveys/${surveyId}/responses/${responseId}`, { + method: "DELETE", + }); + + return response.json(); +}; 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 47a83b3a0a..36269f7245 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/product/index.ts @@ -1,4 +1,4 @@ -import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getSessionUser, hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; import { EnvironmentType } from "@prisma/client"; import { populateEnvironment } from "@/lib/populate"; @@ -6,6 +6,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { const environmentId = req.query?.environmentId?.toString(); + const currentUser: any = await getSessionUser(req, res); const hasAccess = await hasEnvironmentAccess(req, res, environmentId); if (!hasAccess) { @@ -114,6 +115,38 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) const firstEnvironment = newProduct.environments[0]; res.json(firstEnvironment); } + + // DELETE + else if (req.method === "DELETE") { + const membership = await prisma.membership.findUnique({ + where: { + userId_teamId: { + userId: currentUser.id, + teamId: currentUser.teamId, + }, + }, + }); + if (membership?.role !== "admin" && membership?.role !== "owner") { + return res.status(403).json({ message: "You are not allowed to delete products." }); + } + const environment = await prisma.environment.findUnique({ + where: { id: environmentId }, + select: { + productId: true, + }, + }); + + if (environment === null) { + return res.status(404).json({ message: "This environment doesn't exist" }); + } + + // Delete the product with + const prismaRes = await prisma.product.delete({ + where: { id: environment.productId }, + }); + + return res.json(prismaRes); + } // Unknown HTTP Method else { diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/index.ts index d021507935..d578d35388 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/[submissionId]/index.ts @@ -39,8 +39,9 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) // Delete /api/environments[environmentId]/surveys/[surveyId]/responses/[responseId] // Deletes a single survey else if (req.method === "DELETE") { + const submissionId = req.query.submissionId?.toString(); const prismaRes = await prisma.response.delete({ - where: { id: responseId }, + where: { id: submissionId }, }); return res.json(prismaRes); }