diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/DeleteProject.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/DeleteProject.tsx new file mode 100644 index 00000000..cb15e392 --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/DeleteProject.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { deleteProject } from '@/data/user/projects'; +import { useSAToastMutation } from '@/hooks/useSAToastMutation'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { motion } from 'framer-motion'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +type DeleteProjectProps = { + projectName: string; + projectId: string; +}; + +export const DeleteProject = ({ + projectName, + projectId, +}: DeleteProjectProps) => { + const [open, setOpen] = useState(false); + const router = useRouter(); + + const formSchema = z.object({ + projectName: z + .string() + .refine( + (v) => v === `delete ${projectName}`, + `Must match "delete ${projectName}"`, + ), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + projectName: '', + }, + }); + + const { mutate, isLoading } = useSAToastMutation( + async () => deleteProject(projectId), + { + onSuccess: () => { + toast.success('Project deleted'); + setOpen(false); + router.push('/dashboard'); + }, + loadingMessage: 'Deleting project...', + errorMessage(error) { + try { + if (error instanceof Error) { + return String(error.message); + } + return `Failed to delete project ${String(error)}`; + } catch (_err) { + console.warn(_err); + return 'Failed to delete project'; + } + }, + }, + ); + + const onSubmit = (values: z.infer) => { + mutate(); + }; + + return ( + + + + + Danger Zone + + + Deleting a project does not destroy associated resources! Make sure to destroy them. + + + + + + + + + + Delete Project + + Type "delete {projectName}" to confirm. + + + +
+ ( + + Confirmation + + + + + + )} + /> + + + + + +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx index e080109d..c6b718a7 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx @@ -13,6 +13,7 @@ import { Tables } from "@/lib/database.types"; import { motion } from "framer-motion"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; +import { DeleteProject } from "./DeleteProject"; type ProjectSettingsProps = { project: Tables<'projects'>; @@ -291,6 +292,7 @@ export default function ProjectSettings({ project, repositoryName }: ProjectSett + ); } \ No newline at end of file diff --git a/src/data/user/projects.tsx b/src/data/user/projects.tsx index e1bac196..f31a5502 100644 --- a/src/data/user/projects.tsx +++ b/src/data/user/projects.tsx @@ -286,7 +286,8 @@ export async function getProjectsForUser({ .from('projects') .select('*, teams(name)') .eq('organization_id', organizationId) - .ilike('name', `%${query}%`); + .ilike('name', `%${query}%`) + .is('deleted_at', null); if (userRole !== 'admin' || userId !== 'owner') { // For non-admin users, get their team memberships @@ -333,7 +334,8 @@ export async function getProjectsListForUser({ .from('projects') .select('id, name, slug, latest_action_on, created_at, repo_id, latest_drift_output') .eq('organization_id', organizationId) - .ilike('name', `%${query}%`); + .ilike('name', `%${query}%`) + .is('deleted_at', null); if (userRole !== 'admin' || userId !== 'owner') { // For non-admin users, get their team memberships @@ -399,7 +401,8 @@ export async function getSlimProjectsForUser({ let supabaseQuery = supabase .from('projects') .select('id,name, slug, latest_action_on, created_at, repo_id') - .in('id', projectIds); + .in('id', projectIds) + .is('deleted_at', null); if (userRole !== 'admin' || userId !== 'owner') { // For non-admin users, get their team memberships @@ -463,7 +466,8 @@ export async function getProjectsIdsListForUser({ .from('projects') .select('id,name, slug, latest_action_on, created_at, repo_id') .eq('organization_id', organizationId) - .ilike('name', `%${query}%`); + .ilike('name', `%${query}%`) + .is('deleted_at', null); if (userRole !== 'admin' || userId !== 'owner') { // For non-admin users, get their team memberships @@ -538,7 +542,8 @@ export async function getProjectsCountForUser({ .from('projects') .select('*', { count: 'exact', head: true }) .eq('organization_id', organizationId) - .ilike('name', `%${query}%`); + .ilike('name', `%${query}%`) + .is('deleted_at', null); if (userRole.member_role !== 'admin') { // For non-admin users, get their team memberships @@ -585,6 +590,7 @@ export const getAllProjectsInOrganization = async ({ .from("projects") .select("*") .eq("organization_id", organizationId) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); if (query) { @@ -609,6 +615,7 @@ export const getAllProjectIdsInOrganization = async (organizationId: string) => .from("projects") .select("id") .eq("organization_id", organizationId) + .is('deleted_at', null) .order("created_at", { ascending: false }); const { data, error } = await supabaseQuery; @@ -626,6 +633,7 @@ export const getProjectIdsInOrganization = async (organizationId: string, count: const supabaseQuery = supabase .from("projects") .select("id") + .is('deleted_at', null) .eq("organization_id", organizationId); const { data, error } = await supabaseQuery; @@ -660,6 +668,7 @@ export const getOrganizationLevelProjects = async ({ .select("*") .eq("organization_id", organizationId) .is('team_id', null) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); if (query) { @@ -698,6 +707,7 @@ export const getProjects = async ({ .from("projects") .select("*") .eq("organization_id", organizationId) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); // Add team filter @@ -742,6 +752,7 @@ export const getAllProjectsListInOrganization = async ({ .from("projects") .select("id,name, slug, latest_action_on, created_at, repo_id") .eq("organization_id", organizationId) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); if (query) { @@ -793,6 +804,7 @@ export const getProjectsList = async ({ .from("projects") .select("id,name, slug, latest_action_on, created_at, repo_id") .eq("organization_id", organizationId) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); // Add team filter @@ -891,6 +903,7 @@ export const getProjectsForUserTotalCount = async ({ head: true, }) .eq("organization_id", organizationId) + .is('deleted_at', null) .range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1); if (query) {