@@ -74,8 +76,6 @@ const getRisk = cache(async (riskId: string) => {
return risk;
});
-
-
const getAssignees = cache(async () => {
const session = await auth.api.getSession({
headers: await headers(),
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/regenerate-vendor-mitigation.ts b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/regenerate-vendor-mitigation.ts
new file mode 100644
index 000000000..1d01d08e3
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/actions/regenerate-vendor-mitigation.ts
@@ -0,0 +1,35 @@
+'use server';
+
+import { authActionClient } from '@/actions/safe-action';
+import { generateVendorMitigation } from '@/jobs/tasks/onboarding/generate-vendor-mitigation';
+import { tasks } from '@trigger.dev/sdk';
+import { z } from 'zod';
+
+export const regenerateVendorMitigationAction = authActionClient
+ .inputSchema(
+ z.object({
+ vendorId: z.string().min(1),
+ }),
+ )
+ .metadata({
+ name: 'regenerate-vendor-mitigation',
+ track: {
+ event: 'regenerate-vendor-mitigation',
+ channel: 'server',
+ },
+ })
+ .action(async ({ parsedInput, ctx }) => {
+ const { vendorId } = parsedInput;
+ const { session } = ctx;
+
+ if (!session?.activeOrganizationId) {
+ throw new Error('No active organization');
+ }
+
+ await tasks.trigger
('generate-vendor-mitigation', {
+ organizationId: session.activeOrganizationId,
+ vendorId,
+ });
+
+ return { success: true };
+ });
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorActions.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorActions.tsx
new file mode 100644
index 000000000..287314252
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorActions.tsx
@@ -0,0 +1,77 @@
+'use client';
+
+import { regenerateVendorMitigationAction } from '@/app/(app)/[orgId]/vendors/[vendorId]/actions/regenerate-vendor-mitigation';
+import { Button } from '@comp/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@comp/ui/dialog';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@comp/ui/dropdown-menu';
+import { Cog } from 'lucide-react';
+import { useAction } from 'next-safe-action/hooks';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+export function VendorActions({ vendorId }: { vendorId: string }) {
+ const [isConfirmOpen, setIsConfirmOpen] = useState(false);
+ const regenerate = useAction(regenerateVendorMitigationAction, {
+ onSuccess: () => toast.success('Regeneration triggered. This may take a moment.'),
+ onError: () => toast.error('Failed to trigger mitigation regeneration'),
+ });
+
+ const handleConfirm = () => {
+ setIsConfirmOpen(false);
+ toast.info('Regenerating vendor risk mitigation...');
+ regenerate.execute({ vendorId });
+ };
+
+ return (
+ <>
+
+
+
+
+
+ setIsConfirmOpen(true)}>
+ Regenerate Risk Mitigation
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx
index 0cc00a95d..d1fd53ab9 100644
--- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx
+++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx
@@ -8,6 +8,7 @@ import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { cache } from 'react';
import { Comments } from '../../../../../components/comments/Comments';
+import { VendorActions } from './components/VendorActions';
import { VendorInherentRiskChart } from './components/VendorInherentRiskChart';
import { VendorResidualRiskChart } from './components/VendorResidualRiskChart';
import { SecondaryFields } from './components/secondary-fields/secondary-fields';
@@ -31,6 +32,7 @@ export default async function VendorPage({ params }: PageProps) {
{ label: 'Vendors', href: `/${orgId}/vendors` },
{ label: vendor.vendor?.name ?? '', current: true },
]}
+ headerRight={}
>
{
- if (window.confirm('Are you sure you want to delete this comment?')) {
- try {
- // Use API hook directly instead of server action
- await deleteComment(comment.id);
-
- toast.success('Comment deleted successfully.');
- refreshComments();
- } catch (error) {
- toast.error('Failed to delete comment.');
- console.error('Delete comment error:', error);
- }
+ setIsDeleting(true);
+ try {
+ await deleteComment(comment.id);
+ toast.success('Comment deleted successfully.');
+ refreshComments();
+ setIsDeleteOpen(false);
+ } catch (error) {
+ toast.error('Failed to delete comment.');
+ console.error('Delete comment error:', error);
+ } finally {
+ setIsDeleting(false);
}
};
@@ -171,7 +181,7 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) {
setIsDeleteOpen(true)}
>
Delete
@@ -229,6 +239,25 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) {
+ {/* Delete confirmation dialog */}
+