Skip to content

Commit 86b072d

Browse files
committed
Delete projects
1 parent 9efa9b2 commit 86b072d

File tree

3 files changed

+164
-5
lines changed

3 files changed

+164
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use client';
2+
3+
import { Button } from '@/components/ui/button';
4+
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogFooter,
10+
DialogHeader,
11+
DialogTitle,
12+
DialogTrigger,
13+
} from '@/components/ui/dialog';
14+
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
15+
import { Input } from '@/components/ui/input';
16+
import { deleteProject } from '@/data/user/projects';
17+
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
18+
import { zodResolver } from '@hookform/resolvers/zod';
19+
import { motion } from 'framer-motion';
20+
import { useRouter } from 'next/navigation';
21+
import { useState } from 'react';
22+
import { FormProvider, useForm } from 'react-hook-form';
23+
import { toast } from 'sonner';
24+
import { z } from 'zod';
25+
26+
type DeleteProjectProps = {
27+
projectName: string;
28+
projectId: string;
29+
};
30+
31+
export const DeleteProject = ({
32+
projectName,
33+
projectId,
34+
}: DeleteProjectProps) => {
35+
const [open, setOpen] = useState(false);
36+
const router = useRouter();
37+
38+
const formSchema = z.object({
39+
projectName: z
40+
.string()
41+
.refine(
42+
(v) => v === `delete ${projectName}`,
43+
`Must match "delete ${projectName}"`,
44+
),
45+
});
46+
47+
const form = useForm<z.infer<typeof formSchema>>({
48+
resolver: zodResolver(formSchema),
49+
defaultValues: {
50+
projectName: '',
51+
},
52+
});
53+
54+
const { mutate, isLoading } = useSAToastMutation(
55+
async () => deleteProject(projectId),
56+
{
57+
onSuccess: () => {
58+
toast.success('Project deleted');
59+
setOpen(false);
60+
router.push('/dashboard');
61+
},
62+
loadingMessage: 'Deleting project...',
63+
errorMessage(error) {
64+
try {
65+
if (error instanceof Error) {
66+
return String(error.message);
67+
}
68+
return `Failed to delete project ${String(error)}`;
69+
} catch (_err) {
70+
console.warn(_err);
71+
return 'Failed to delete project';
72+
}
73+
},
74+
},
75+
);
76+
77+
const onSubmit = (values: z.infer<typeof formSchema>) => {
78+
mutate();
79+
};
80+
81+
return (
82+
<motion.div
83+
initial={{ opacity: 0, y: 20 }}
84+
animate={{ opacity: 1, y: 0 }}
85+
transition={{ duration: 0.3 }}
86+
className="mt-4"
87+
>
88+
<Card className="w-full max-w-5xl border-destructive/50 bg-destructive/5">
89+
<CardHeader>
90+
<CardTitle>
91+
Danger Zone
92+
</CardTitle>
93+
<CardDescription>
94+
Deleting a project does not destroy associated resources! Make sure to destroy them.
95+
</CardDescription>
96+
</CardHeader>
97+
<CardFooter className="flex justify-start">
98+
<Dialog open={open} onOpenChange={setOpen}>
99+
<DialogTrigger asChild>
100+
<Button variant="destructive">Delete Project</Button>
101+
</DialogTrigger>
102+
<DialogContent>
103+
<DialogHeader>
104+
<DialogTitle>Delete Project</DialogTitle>
105+
<DialogDescription>
106+
Type <strong>"delete {projectName}"</strong> to confirm.
107+
</DialogDescription>
108+
</DialogHeader>
109+
<FormProvider {...form}>
110+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
111+
<FormField
112+
control={form.control}
113+
name="projectName"
114+
render={({ field }) => (
115+
<FormItem>
116+
<FormLabel>Confirmation</FormLabel>
117+
<FormControl>
118+
<Input {...field} />
119+
</FormControl>
120+
<FormMessage />
121+
</FormItem>
122+
)}
123+
/>
124+
<DialogFooter>
125+
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
126+
Cancel
127+
</Button>
128+
<Button
129+
type="submit"
130+
variant="destructive"
131+
disabled={isLoading || !form.formState.isValid}
132+
>
133+
{isLoading ? 'Deleting...' : 'Delete'} Project
134+
</Button>
135+
</DialogFooter>
136+
</form>
137+
</FormProvider>
138+
</DialogContent>
139+
</Dialog>
140+
</CardFooter>
141+
</Card>
142+
</motion.div>
143+
);
144+
};

src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ProjectSettings.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Tables } from "@/lib/database.types";
1313
import { motion } from "framer-motion";
1414
import { useState } from "react";
1515
import { Controller, useForm } from "react-hook-form";
16+
import { DeleteProject } from "./DeleteProject";
1617

1718
type ProjectSettingsProps = {
1819
project: Tables<'projects'>;
@@ -291,6 +292,7 @@ export default function ProjectSettings({ project, repositoryName }: ProjectSett
291292
</form>
292293
</CardContent>
293294
</Card>
295+
<DeleteProject projectName={project.name} projectId={project.id} />
294296
</motion.div>
295297
);
296298
}

src/data/user/projects.tsx

+18-5
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ export async function getProjectsForUser({
286286
.from('projects')
287287
.select('*, teams(name)')
288288
.eq('organization_id', organizationId)
289-
.ilike('name', `%${query}%`);
289+
.ilike('name', `%${query}%`)
290+
.is('deleted_at', null);
290291

291292
if (userRole !== 'admin' || userId !== 'owner') {
292293
// For non-admin users, get their team memberships
@@ -333,7 +334,8 @@ export async function getProjectsListForUser({
333334
.from('projects')
334335
.select('id, name, slug, latest_action_on, created_at, repo_id, latest_drift_output')
335336
.eq('organization_id', organizationId)
336-
.ilike('name', `%${query}%`);
337+
.ilike('name', `%${query}%`)
338+
.is('deleted_at', null);
337339

338340
if (userRole !== 'admin' || userId !== 'owner') {
339341
// For non-admin users, get their team memberships
@@ -399,7 +401,8 @@ export async function getSlimProjectsForUser({
399401
let supabaseQuery = supabase
400402
.from('projects')
401403
.select('id,name, slug, latest_action_on, created_at, repo_id')
402-
.in('id', projectIds);
404+
.in('id', projectIds)
405+
.is('deleted_at', null);
403406

404407
if (userRole !== 'admin' || userId !== 'owner') {
405408
// For non-admin users, get their team memberships
@@ -463,7 +466,8 @@ export async function getProjectsIdsListForUser({
463466
.from('projects')
464467
.select('id,name, slug, latest_action_on, created_at, repo_id')
465468
.eq('organization_id', organizationId)
466-
.ilike('name', `%${query}%`);
469+
.ilike('name', `%${query}%`)
470+
.is('deleted_at', null);
467471

468472
if (userRole !== 'admin' || userId !== 'owner') {
469473
// For non-admin users, get their team memberships
@@ -538,7 +542,8 @@ export async function getProjectsCountForUser({
538542
.from('projects')
539543
.select('*', { count: 'exact', head: true })
540544
.eq('organization_id', organizationId)
541-
.ilike('name', `%${query}%`);
545+
.ilike('name', `%${query}%`)
546+
.is('deleted_at', null);
542547

543548
if (userRole.member_role !== 'admin') {
544549
// For non-admin users, get their team memberships
@@ -585,6 +590,7 @@ export const getAllProjectsInOrganization = async ({
585590
.from("projects")
586591
.select("*")
587592
.eq("organization_id", organizationId)
593+
.is('deleted_at', null)
588594
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
589595

590596
if (query) {
@@ -609,6 +615,7 @@ export const getAllProjectIdsInOrganization = async (organizationId: string) =>
609615
.from("projects")
610616
.select("id")
611617
.eq("organization_id", organizationId)
618+
.is('deleted_at', null)
612619
.order("created_at", { ascending: false });
613620

614621
const { data, error } = await supabaseQuery;
@@ -626,6 +633,7 @@ export const getProjectIdsInOrganization = async (organizationId: string, count:
626633
const supabaseQuery = supabase
627634
.from("projects")
628635
.select("id")
636+
.is('deleted_at', null)
629637
.eq("organization_id", organizationId);
630638

631639
const { data, error } = await supabaseQuery;
@@ -660,6 +668,7 @@ export const getOrganizationLevelProjects = async ({
660668
.select("*")
661669
.eq("organization_id", organizationId)
662670
.is('team_id', null)
671+
.is('deleted_at', null)
663672
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
664673

665674
if (query) {
@@ -698,6 +707,7 @@ export const getProjects = async ({
698707
.from("projects")
699708
.select("*")
700709
.eq("organization_id", organizationId)
710+
.is('deleted_at', null)
701711
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
702712

703713
// Add team filter
@@ -742,6 +752,7 @@ export const getAllProjectsListInOrganization = async ({
742752
.from("projects")
743753
.select("id,name, slug, latest_action_on, created_at, repo_id")
744754
.eq("organization_id", organizationId)
755+
.is('deleted_at', null)
745756
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
746757

747758
if (query) {
@@ -793,6 +804,7 @@ export const getProjectsList = async ({
793804
.from("projects")
794805
.select("id,name, slug, latest_action_on, created_at, repo_id")
795806
.eq("organization_id", organizationId)
807+
.is('deleted_at', null)
796808
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
797809

798810
// Add team filter
@@ -891,6 +903,7 @@ export const getProjectsForUserTotalCount = async ({
891903
head: true,
892904
})
893905
.eq("organization_id", organizationId)
906+
.is('deleted_at', null)
894907
.range(zeroIndexedPage * limit, (zeroIndexedPage + 1) * limit - 1);
895908

896909
if (query) {

0 commit comments

Comments
 (0)