mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
Add ability to delete products and responses (#276)
* #263 long strings edge cases fixed * Add ability to delete products and responses --------- Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
eea8500501
commit
c352c9ff5d
@@ -280,7 +280,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel className="cursor-default break-all">
|
||||
<span className="ph-no-capture font-normal">Signed in as </span>
|
||||
{session.user.name.length > 30 ? (
|
||||
{session?.user?.name.length > 30 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function PersonDetails({ environmentId, personId }: PersonDetails
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}>
|
||||
<TrashIcon className="h-5 w-5 text-slate-500 hover:text-red-500" />
|
||||
<TrashIcon className="h-5 w-5 text-slate-500 hover:text-red-700" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 }) {
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
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 <LoadingSpinner />;
|
||||
}
|
||||
if (isErrorProduct) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<p className="text-sm text-slate-900">
|
||||
Here you can delete
|
||||
<strong>{truncate(product?.name, 30)}</strong>
|
||||
incl. all surveys, responses, people, actions and attributes.{" "}
|
||||
<strong>This action cannot be undone.</strong>
|
||||
</p>
|
||||
<Button disabled={isDeleteDisabled} variant="warn" className={`mt-4 ${isDeleteDisabled ? 'ring-1 ring-offset-1 ring-grey-500' : ''}`} onClick={() => setIsDeleteDialogOpen(true)}>
|
||||
Delete
|
||||
</Button>
|
||||
{isDeleteDisabled && (
|
||||
<p className="text-xs text-red-700 mt-2">
|
||||
{!isUserAdminOrOwner
|
||||
? 'Only Admin or Owners can delete products.'
|
||||
: 'Environment needs at least 1 product.'
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<DeleteDialog
|
||||
deleteWhat="Product"
|
||||
open={isDeleteDialogOpen}
|
||||
setOpen={setIsDeleteDialogOpen}
|
||||
onDelete={handleDeleteProduct}
|
||||
text="Are you sure you want to delete this Product? This action cannot be undone."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.">
|
||||
<EditWaitingTime environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="Danger Zone"
|
||||
description="You will delete all surveys, responses, people, actions and attributes along with the product.">
|
||||
<DeleteProduct environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function ResponseTimeline({ environmentId, surveyId }) {
|
||||
<div>
|
||||
{matchQandA.map((updatedResponse) => {
|
||||
return (
|
||||
<SingleResponse key={updatedResponse.id} data={updatedResponse} environmentId={environmentId} />
|
||||
<SingleResponse key={updatedResponse.id} data={updatedResponse} surveyId={surveyId} environmentId={environmentId} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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 <CheckCircleIcon className="ml-1 h-5 w-5 text-green-400" />
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}>
|
||||
<TrashIcon className="h-4 w-4 text-slate-500 hover:text-red-700" />
|
||||
</button>
|
||||
</div>
|
||||
<time className="text-slate-500" dateTime={timeSince(data.updatedAt)}>
|
||||
{timeSince(data.updatedAt)}
|
||||
</time>
|
||||
@@ -91,6 +116,12 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DeleteDialog
|
||||
open={deleteDialogOpen}
|
||||
setOpen={setDeleteDialogOpen}
|
||||
deleteWhat="response"
|
||||
onDelete={handleDeleteSubmission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user