Skip to content

Commit 155bf29

Browse files
authored
Merge pull request #698 from trycompai/mariano/comp-147-ability-to-delete-polices-controls-and-frameworks
[dev] [Marfuen] mariano/comp-147-ability-to-delete-polices-controls-and-frameworks
2 parents b729a9d + 9fd5c23 commit 155bf29

File tree

11 files changed

+760
-70
lines changed

11 files changed

+760
-70
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use server";
2+
3+
import { db } from "@comp/db";
4+
import { revalidatePath, revalidateTag } from "next/cache";
5+
import { z } from "zod";
6+
import { authActionClient } from "../safe-action";
7+
8+
const deletePolicySchema = z.object({
9+
id: z.string(),
10+
entityId: z.string(),
11+
});
12+
13+
export const deletePolicyAction = authActionClient
14+
.schema(deletePolicySchema)
15+
.metadata({
16+
name: "delete-policy",
17+
track: {
18+
event: "delete-policy",
19+
description: "Delete Policy",
20+
channel: "server",
21+
},
22+
})
23+
.action(async ({ parsedInput, ctx }) => {
24+
const { id } = parsedInput;
25+
const { activeOrganizationId } = ctx.session;
26+
27+
if (!activeOrganizationId) {
28+
return {
29+
success: false,
30+
error: "Not authorized",
31+
};
32+
}
33+
34+
try {
35+
const policy = await db.policy.findUnique({
36+
where: {
37+
id,
38+
organizationId: activeOrganizationId,
39+
},
40+
});
41+
42+
if (!policy) {
43+
return {
44+
success: false,
45+
error: "Policy not found",
46+
};
47+
}
48+
49+
// Delete the policy
50+
await db.policy.delete({
51+
where: { id },
52+
});
53+
54+
// Revalidate paths to update UI
55+
revalidatePath(`/${activeOrganizationId}/policies/all`);
56+
revalidatePath(`/${activeOrganizationId}/policies`);
57+
revalidateTag("policies");
58+
59+
return {
60+
success: true,
61+
};
62+
} catch (error) {
63+
console.error(error);
64+
return {
65+
success: false,
66+
error: "Failed to delete policy",
67+
};
68+
}
69+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use server";
2+
3+
import { db } from "@comp/db";
4+
import { revalidatePath, revalidateTag } from "next/cache";
5+
import { z } from "zod";
6+
import { authActionClient } from "../../../../../../../../actions/safe-action";
7+
8+
const deleteControlSchema = z.object({
9+
id: z.string(),
10+
entityId: z.string(),
11+
});
12+
13+
export const deleteControlAction = authActionClient
14+
.schema(deleteControlSchema)
15+
.metadata({
16+
name: "delete-control",
17+
track: {
18+
event: "delete-control",
19+
description: "Delete Control",
20+
channel: "server",
21+
},
22+
})
23+
.action(async ({ parsedInput, ctx }) => {
24+
const { id } = parsedInput;
25+
const { activeOrganizationId } = ctx.session;
26+
27+
if (!activeOrganizationId) {
28+
return {
29+
success: false,
30+
error: "Not authorized",
31+
};
32+
}
33+
34+
try {
35+
const control = await db.control.findUnique({
36+
where: {
37+
id,
38+
organizationId: activeOrganizationId,
39+
},
40+
});
41+
42+
if (!control) {
43+
return {
44+
success: false,
45+
error: "Control not found",
46+
};
47+
}
48+
49+
// Delete the control
50+
await db.control.delete({
51+
where: { id },
52+
});
53+
54+
// Revalidate paths to update UI
55+
revalidatePath(`/${activeOrganizationId}/controls/all`);
56+
revalidatePath(`/${activeOrganizationId}/controls`);
57+
revalidateTag("controls");
58+
59+
return {
60+
success: true,
61+
};
62+
} catch (error) {
63+
console.error(error);
64+
return {
65+
success: false,
66+
error: "Failed to delete control",
67+
};
68+
}
69+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"use client";
2+
3+
import { deleteControlAction } from "@/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/actions/delete-control";
4+
import { Control } from "@comp/db/types";
5+
import { Button } from "@comp/ui/button";
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogFooter,
11+
DialogHeader,
12+
DialogTitle,
13+
} from "@comp/ui/dialog";
14+
import { Form } from "@comp/ui/form";
15+
import { zodResolver } from "@hookform/resolvers/zod";
16+
import { Trash2 } from "lucide-react";
17+
import { useAction } from "next-safe-action/hooks";
18+
import { useRouter } from "next/navigation";
19+
import { useState } from "react";
20+
import { useForm } from "react-hook-form";
21+
import { toast } from "sonner";
22+
import { z } from "zod";
23+
24+
const formSchema = z.object({
25+
comment: z.string().optional(),
26+
});
27+
28+
type FormValues = z.infer<typeof formSchema>;
29+
30+
interface ControlDeleteDialogProps {
31+
isOpen: boolean;
32+
onClose: () => void;
33+
control: Control;
34+
}
35+
36+
export function ControlDeleteDialog({
37+
isOpen,
38+
onClose,
39+
control,
40+
}: ControlDeleteDialogProps) {
41+
const router = useRouter();
42+
const [isSubmitting, setIsSubmitting] = useState(false);
43+
44+
const form = useForm<FormValues>({
45+
resolver: zodResolver(formSchema),
46+
defaultValues: {
47+
comment: "",
48+
},
49+
});
50+
51+
const deleteControl = useAction(deleteControlAction, {
52+
onSuccess: () => {
53+
toast.info("Control deleted! Redirecting to controls list...");
54+
onClose();
55+
router.push(`/${control.organizationId}/controls`);
56+
},
57+
onError: () => {
58+
toast.error("Failed to delete control.");
59+
setIsSubmitting(false);
60+
},
61+
});
62+
63+
const handleSubmit = async (values: FormValues) => {
64+
setIsSubmitting(true);
65+
deleteControl.execute({
66+
id: control.id,
67+
entityId: control.id,
68+
});
69+
};
70+
71+
return (
72+
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
73+
<DialogContent className="sm:max-w-[425px]">
74+
<DialogHeader>
75+
<DialogTitle>Delete Control</DialogTitle>
76+
<DialogDescription>
77+
Are you sure you want to delete this control? This
78+
action cannot be undone.
79+
</DialogDescription>
80+
</DialogHeader>
81+
<Form {...form}>
82+
<form
83+
onSubmit={form.handleSubmit(handleSubmit)}
84+
className="space-y-4"
85+
>
86+
<DialogFooter className="gap-2">
87+
<Button
88+
type="button"
89+
variant="outline"
90+
onClick={onClose}
91+
disabled={isSubmitting}
92+
>
93+
Cancel
94+
</Button>
95+
<Button
96+
type="submit"
97+
variant="destructive"
98+
disabled={isSubmitting}
99+
className="gap-2"
100+
>
101+
{isSubmitting ? (
102+
<span className="flex items-center gap-2">
103+
<span className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
104+
Deleting...
105+
</span>
106+
) : (
107+
<span className="flex items-center gap-2">
108+
<Trash2 className="h-3 w-3" />
109+
Delete
110+
</span>
111+
)}
112+
</Button>
113+
</DialogFooter>
114+
</form>
115+
</Form>
116+
</DialogContent>
117+
</Dialog>
118+
);
119+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/components/SingleControl.tsx

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ import type {
1212
Task,
1313
} from "@comp/db/types";
1414
import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card";
15+
import { Button } from "@comp/ui/button";
16+
import {
17+
DropdownMenu,
18+
DropdownMenuContent,
19+
DropdownMenuItem,
20+
DropdownMenuTrigger,
21+
} from "@comp/ui/dropdown-menu";
22+
import { MoreVertical, PencilIcon, Trash2 } from "lucide-react";
23+
import { useState } from "react";
24+
import { ControlDeleteDialog } from "./ControlDeleteDialog";
1525
import { useParams } from "next/navigation";
1626
import { useMemo } from "react";
1727
import type { ControlProgressResponse } from "../data/getOrganizationControlProgress";
@@ -34,16 +44,18 @@ interface SingleControlProps {
3444
relatedTasks: Task[];
3545
}
3646

37-
export const SingleControl = ({
47+
export function SingleControl({
3848
control,
3949
controlProgress,
40-
relatedPolicies = [],
41-
relatedTasks = [],
42-
}: SingleControlProps) => {
50+
relatedPolicies,
51+
relatedTasks,
52+
}: SingleControlProps) {
4353
const t = useI18n();
44-
const params = useParams();
45-
const orgId = params.orgId as string;
46-
const controlId = params.controlId as string;
54+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
55+
const [dropdownOpen, setDropdownOpen] = useState(false);
56+
const params = useParams<{ orgId: string; controlId: string }>();
57+
const orgIdFromParams = params.orgId;
58+
const controlIdFromParams = params.controlId;
4759

4860
const progressStatus = useMemo(() => {
4961
if (!controlProgress) return "not_started";
@@ -80,7 +92,32 @@ export const SingleControl = ({
8092
{control.name}
8193
</h1>
8294
</div>
83-
<StatusIndicator status={progressStatus} />
95+
<div className="flex items-center gap-2">
96+
<StatusIndicator status={progressStatus} />
97+
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
98+
<DropdownMenuTrigger asChild>
99+
<Button
100+
size="icon"
101+
variant="ghost"
102+
className="p-2 m-0 size-auto"
103+
>
104+
<MoreVertical className="h-4 w-4" />
105+
</Button>
106+
</DropdownMenuTrigger>
107+
<DropdownMenuContent align="end">
108+
<DropdownMenuItem
109+
onClick={() => {
110+
setDropdownOpen(false);
111+
setDeleteDialogOpen(true);
112+
}}
113+
className="text-destructive focus:text-destructive"
114+
>
115+
<Trash2 className="h-4 w-4 mr-2" />
116+
Delete
117+
</DropdownMenuItem>
118+
</DropdownMenuContent>
119+
</DropdownMenu>
120+
</div>
84121
</CardTitle>
85122
</CardHeader>
86123
<CardContent>
@@ -91,18 +128,25 @@ export const SingleControl = ({
91128
</Card>
92129
<RequirementsTable
93130
requirements={control.requirementsMapped}
94-
orgId={orgId}
131+
orgId={orgIdFromParams}
95132
/>
96133
<PoliciesTable
97134
policies={relatedPolicies}
98-
orgId={orgId}
99-
controlId={controlId}
135+
orgId={orgIdFromParams}
136+
controlId={controlIdFromParams}
100137
/>
101138
<TasksTable
102139
tasks={relatedTasks}
103-
orgId={orgId}
104-
controlId={controlId}
140+
orgId={orgIdFromParams}
141+
controlId={controlIdFromParams}
142+
/>
143+
144+
{/* Delete Dialog */}
145+
<ControlDeleteDialog
146+
isOpen={deleteDialogOpen}
147+
onClose={() => setDeleteDialogOpen(false)}
148+
control={control}
105149
/>
106150
</div>
107151
);
108-
};
152+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/controls/[controlId]/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ export default async function ControlPage({ params }: ControlPageProps) {
5353
controlId: controlId,
5454
});
5555

56-
console.log(relatedPolicies);
57-
5856
return (
5957
<PageWithBreadcrumb
6058
breadcrumbs={[

0 commit comments

Comments
 (0)