Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
case 'QUEUED':
case 'EXECUTING':
case 'PENDING_VERSION':
case 'DEQUEUED':
case 'DELAYED':
return (
<div className="flex flex-col items-center justify-center gap-2 text-center">
Expand Down Expand Up @@ -156,7 +157,6 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
case 'CANCELED':
case 'CRASHED':
case 'SYSTEM_FAILURE':
case 'DEQUEUED':
case 'EXPIRED':
case 'TIMED_OUT': {
const errorMessage = run.error?.message || 'An unexpected issue occurred.';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use server';

import { authActionClient } from '@/actions/safe-action';
import { generateRiskMitigation } from '@/jobs/tasks/onboarding/generate-risk-mitigation';
import { tasks } from '@trigger.dev/sdk';
import { z } from 'zod';

export const regenerateRiskMitigationAction = authActionClient
.inputSchema(
z.object({
riskId: z.string().min(1),
}),
)
.metadata({
name: 'regenerate-risk-mitigation',
track: {
event: 'regenerate-risk-mitigation',
channel: 'server',
},
})
.action(async ({ parsedInput, ctx }) => {
const { riskId } = parsedInput;
const { session } = ctx;

if (!session?.activeOrganizationId) {
throw new Error('No active organization');
}

await tasks.trigger<typeof generateRiskMitigation>('generate-risk-mitigation', {
organizationId: session.activeOrganizationId,
riskId,
});

return { success: true };
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use client';

import { regenerateRiskMitigationAction } from '@/app/(app)/[orgId]/risk/[riskId]/actions/regenerate-risk-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 RiskActions({ riskId }: { riskId: string }) {
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const regenerate = useAction(regenerateRiskMitigationAction, {
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 risk mitigation...');
regenerate.execute({ riskId });
};

return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Risk actions">
<Cog className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsConfirmOpen(true)}>
Regenerate Risk Mitigation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<Dialog open={isConfirmOpen} onOpenChange={(open) => !open && setIsConfirmOpen(false)}>
<DialogContent className="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>Regenerate Mitigation</DialogTitle>
<DialogDescription>
This will generate a fresh mitigation comment for this risk and mark it closed.
Continue?
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setIsConfirmOpen(false)}
disabled={regenerate.status === 'executing'}
>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={regenerate.status === 'executing'}>
{regenerate.status === 'executing' ? 'Working…' : 'Confirm'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
4 changes: 2 additions & 2 deletions apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { cache } from 'react';
import { Comments } from '../../../../../components/comments/Comments';
import { RiskActions } from './components/RiskActions';

interface PageProps {
searchParams: Promise<{
Expand All @@ -35,6 +36,7 @@ export default async function RiskPage({ searchParams, params }: PageProps) {
{ label: 'Risks', href: `/${orgId}/risk` },
{ label: risk.title, current: true },
]}
headerRight={<RiskActions riskId={riskId} />}
>
<div className="flex flex-col gap-4">
<RiskOverview risk={risk} assignees={assignees} />
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof generateVendorMitigation>('generate-vendor-mitigation', {
organizationId: session.activeOrganizationId,
vendorId,
});

return { success: true };
});
Original file line number Diff line number Diff line change
@@ -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 (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Vendor actions">
<Cog className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsConfirmOpen(true)}>
Regenerate Risk Mitigation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<Dialog open={isConfirmOpen} onOpenChange={(open) => !open && setIsConfirmOpen(false)}>
<DialogContent className="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>Regenerate Mitigation</DialogTitle>
<DialogDescription>
This will generate a fresh risk mitigation comment for this vendor and mark it
assessed. Continue?
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setIsConfirmOpen(false)}
disabled={regenerate.status === 'executing'}
>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={regenerate.status === 'executing'}>
{regenerate.status === 'executing' ? 'Working…' : 'Confirm'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
2 changes: 2 additions & 0 deletions apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +32,7 @@ export default async function VendorPage({ params }: PageProps) {
{ label: 'Vendors', href: `/${orgId}/vendors` },
{ label: vendor.vendor?.name ?? '', current: true },
]}
headerRight={<VendorActions vendorId={vendorId} />}
>
<div className="flex flex-col gap-4">
<SecondaryFields
Expand Down
53 changes: 41 additions & 12 deletions apps/app/src/components/comments/CommentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { useCommentActions } from '@/hooks/use-comments-api';
import { Avatar, AvatarFallback, AvatarImage } from '@comp/ui/avatar';
import { Button } from '@comp/ui/button';
import { Card, CardContent } from '@comp/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@comp/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -54,6 +62,8 @@ interface CommentItemProps {
export function CommentItem({ comment, refreshComments }: CommentItemProps) {
const [isEditing, setIsEditing] = useState(false);
const [editedContent, setEditedContent] = useState(comment.content);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);

// Use API hooks instead of server actions
const { updateComment, deleteComment } = useCommentActions();
Expand Down Expand Up @@ -93,17 +103,17 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) {
};

const handleDeleteComment = async () => {
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);
}
};

Expand Down Expand Up @@ -171,7 +181,7 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) {
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive focus:text-destructive focus:bg-destructive/10"
onSelect={handleDeleteComment}
onSelect={() => setIsDeleteOpen(true)}
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
Delete
Expand Down Expand Up @@ -229,6 +239,25 @@ export function CommentItem({ comment, refreshComments }: CommentItemProps) {
</div>
</div>
</CardContent>
{/* Delete confirmation dialog */}
<Dialog open={isDeleteOpen} onOpenChange={(open) => !open && setIsDeleteOpen(false)}>
<DialogContent className="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>Delete Comment</DialogTitle>
<DialogDescription>
Are you sure you want to delete this comment? This cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={() => setIsDeleteOpen(false)} disabled={isDeleting}>
Cancel
</Button>
<Button variant="destructive" onClick={handleDeleteComment} disabled={isDeleting}>
{isDeleting ? 'Deleting…' : 'Delete'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
}
Loading
Loading