From 11190abafd110ecea3905ea3a4a35f1b227e7ca8 Mon Sep 17 00:00:00 2001 From: lorentz Date: Thu, 16 Jul 2026 14:40:00 -0400 Subject: [PATCH 1/4] feat(22-05): checkbox list + editable params + Approve selected (D-03) - Create ActionAreaCard with a null-guard on classification (renders an informational note, never dereferences recommendedActions, for the default grouped-but-unclassified state) - Render one checkbox row per recommended action with an always-visible params form pre-filled via deriveDefaultParams(actionType, evidence) - Submit exact ApproveActionInput[] to POST /approve; purge_message mailboxes is edited as a comma-separated string and normalized to string[] at submit time - Refetch via onActionComplete() on success (D-04, no optimistic mutation) --- components/phishing/action-area-card.tsx | 367 +++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 components/phishing/action-area-card.tsx diff --git a/components/phishing/action-area-card.tsx b/components/phishing/action-area-card.tsx new file mode 100644 index 0000000..7a8a08f --- /dev/null +++ b/components/phishing/action-area-card.tsx @@ -0,0 +1,367 @@ +'use client'; + +/** + * ActionAreaCard — the only interactive remediation surface on the phishing + * campaign review page (REVIEW-05, REVIEW-06). Renders one checkbox row per + * recommended action with an always-visible, editable params form + * pre-filled via deriveDefaultParams(), and submits exactly + * ApproveActionInput[] to the existing /approve route. + * + * D-04: every action refetches via onActionComplete() on success — this + * component never optimistically mutates local campaign state. + */ + +import { useEffect, useState } from 'react'; +import { Loader2, ShieldAlert } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { deriveDefaultParams } from '@/lib/services/remediation-default-params'; + +export interface RemediationActionSummary { + id: string; + actionType: string; + status: string; + completedAt: string | null; + approvedBy: string | null; +} + +export interface ActionAreaClassification { + recommendedActions: string[]; +} + +export interface ActionAreaEvidence { + requesterEmail: string | null; + senderEmail: string | null; + senderDomain: string | null; + messageId: string | null; +} + +interface ActionAreaCardProps { + campaignId: string; + classification: ActionAreaClassification | null; + remediationActions: RemediationActionSummary[]; + campaignStatus: string; + campaignUpdatedAt: string; + evidence: ActionAreaEvidence; + onActionComplete: () => void; +} + +const ACTION_LABEL: Record = { + block_sender: 'Block sender', + purge_message: 'Purge message', + warn_user: 'Warn user', + no_action: 'No action', + reset_password: 'Reset password', + isolate_endpoint: 'Isolate endpoint', + disable_forwarding_rule: 'Disable forwarding rule', +}; + +function humanizeAction(actionType: string): string { + return ( + ACTION_LABEL[actionType] ?? + actionType + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + ); +} + +interface ActionRowState { + checked: boolean; + params: Record; +} + +/** + * Normalizes a row's editable params back into the submit shape. The only + * field that needs conversion is purge_message's `mailboxes` — edited as a + * comma-separated string in the form, submitted as `string[]` to match + * ApproveActionInput exactly. + */ +function normalizeParamsForSubmit(actionType: string, params: Record): Record { + if (actionType === 'purge_message' && typeof params.mailboxes === 'string') { + return { + ...params, + mailboxes: params.mailboxes + .split(',') + .map((m) => m.trim()) + .filter(Boolean), + }; + } + return params; +} + +function ActionParamsForm({ + actionType, + params, + onChange, +}: { + actionType: string; + params: Record; + onChange: (params: Record) => void; +}) { + function setField(key: string, value: string) { + onChange({ ...params, [key]: value }); + } + + const str = (key: string) => (typeof params[key] === 'string' ? (params[key] as string) : ''); + + switch (actionType) { + case 'no_action': + return ( +

+ No parameters — informational verdict, no remediation needed. +

+ ); + case 'warn_user': + return ( +
+
+ + setField('recipientEmail', e.target.value)} + /> +
+
+ +