Add campaign delete functionality with single and bulk selection

- Add CampaignDeleteConfirmationModal for campaign deletion confirmation
- Add checkbox selection column to CampaignList with select all/indeterminate state
- Add actions column with trash icon for single campaign deletion
- Add bulk actions bar showing selected count with Clear/Delete buttons
- Add handleDeleteCampaign handler in App.tsx using apiService.deleteCampaign
- Pass onDeleteCampaign prop through Campaigns component chain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-25 07:46:51 -06:00
parent 5388c390ed
commit 687edb547c
2 changed files with 242 additions and 12 deletions

View file

@ -510,6 +510,29 @@ const App: React.FC = () => {
}
};
const handleDeleteCampaign = async (campaignName: string) => {
const campaign = campaigns.find(c => c.name === campaignName);
if (!campaign?._id) {
setError('Campaign not found.');
return;
}
try {
await apiService.deleteCampaign(campaign._id);
// Update local state after successful deletion
setCampaigns(prev => prev.filter(c => c.name !== campaignName));
setCampaignProofs(prev => {
const newProofs = { ...prev };
delete newProofs[campaignName];
return newProofs;
});
} catch (error) {
console.error('Error deleting campaign:', error);
setError('Failed to delete campaign. Please try again.');
}
};
// --- SETTINGS HANDLERS (NOW USE API) ---
// Helper to refresh dropdown options from API
@ -791,6 +814,7 @@ const App: React.FC = () => {
onRetryAnalysis={handleRetryAnalysis}
onCampaignStatusChange={handleCampaignStatusChange}
onDeleteProof={handleDeleteProof}
onDeleteCampaign={handleDeleteCampaign}
onFlagSubmit={handleFlagSubmit}
onResolveSubmit={handleResolveSubmit}
/>;

View file

@ -268,16 +268,35 @@ const OverallStatusBadge: React.FC<{ status: OverallStatus }> = ({ status }) =>
};
const CampaignList: React.FC<{
onSelectCampaign: (name: string) => void;
const CampaignList: React.FC<{
onSelectCampaign: (name: string) => void;
campaigns: typeof initialCampaigns;
onOpenModal: () => void;
onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void;
}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange }) => {
onDeleteCampaign: (campaign: typeof initialCampaigns[0]) => void;
}> = ({ onSelectCampaign, campaigns, onOpenModal, onCampaignStatusChange, onDeleteCampaign }) => {
const [showCompleted, setShowCompleted] = useState(true);
const [selectedCampaigns, setSelectedCampaigns] = useState<Set<string>>(new Set());
const [campaignToDelete, setCampaignToDelete] = useState<typeof initialCampaigns[0] | null>(null);
const [showBulkDeleteModal, setShowBulkDeleteModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const filteredCampaigns = showCompleted ? campaigns : campaigns.filter(p => p.status !== 'Completed');
// Clear selection when campaigns list changes (e.g., after deletion)
useEffect(() => {
setSelectedCampaigns(prev => {
const currentNames = new Set(filteredCampaigns.map(c => c.name));
const newSelection = new Set<string>();
prev.forEach(name => {
if (currentNames.has(name)) {
newSelection.add(name);
}
});
return newSelection;
});
}, [filteredCampaigns]);
const getStatusSelectClasses = (status: string) => {
let baseClasses = 'w-full text-center px-2.5 py-0.5 text-xs font-semibold rounded-full border border-transparent focus:outline-none focus:ring-2 focus:ring-brand-accent appearance-none cursor-pointer';
let colorClasses = '';
@ -294,8 +313,77 @@ const CampaignList: React.FC<{
return `${baseClasses} ${colorClasses}`;
};
const handleSelectAll = () => {
if (selectedCampaigns.size === filteredCampaigns.length) {
setSelectedCampaigns(new Set());
} else {
setSelectedCampaigns(new Set(filteredCampaigns.map(c => c.name)));
}
};
const handleSelectCampaign = (campaignName: string, e: React.MouseEvent) => {
e.stopPropagation();
setSelectedCampaigns(prev => {
const newSet = new Set(prev);
if (newSet.has(campaignName)) {
newSet.delete(campaignName);
} else {
newSet.add(campaignName);
}
return newSet;
});
};
const handleSingleDelete = (campaign: typeof initialCampaigns[0], e: React.MouseEvent) => {
e.stopPropagation();
setCampaignToDelete(campaign);
};
const handleConfirmSingleDelete = async () => {
if (campaignToDelete) {
setIsDeleting(true);
await onDeleteCampaign(campaignToDelete);
setCampaignToDelete(null);
setIsDeleting(false);
}
};
const handleConfirmBulkDelete = async () => {
setIsDeleting(true);
const campaignsToDelete = filteredCampaigns.filter(c => selectedCampaigns.has(c.name));
for (const campaign of campaignsToDelete) {
await onDeleteCampaign(campaign);
}
setSelectedCampaigns(new Set());
setShowBulkDeleteModal(false);
setIsDeleting(false);
};
const isAllSelected = filteredCampaigns.length > 0 && selectedCampaigns.size === filteredCampaigns.length;
const isIndeterminate = selectedCampaigns.size > 0 && selectedCampaigns.size < filteredCampaigns.length;
return (
<div className="p-4 sm:p-6 lg:p-8 h-full bg-brand-gray">
<CampaignDeleteConfirmationModal
isOpen={!!campaignToDelete}
onClose={() => setCampaignToDelete(null)}
onConfirm={handleConfirmSingleDelete}
campaignName={campaignToDelete?.name || ''}
proofCount={campaignToDelete?.proofs || 0}
isBulk={false}
selectedCount={1}
/>
<CampaignDeleteConfirmationModal
isOpen={showBulkDeleteModal}
onClose={() => setShowBulkDeleteModal(false)}
onConfirm={handleConfirmBulkDelete}
campaignName=""
proofCount={0}
isBulk={true}
selectedCount={selectedCampaigns.size}
/>
<header className="mb-8">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
@ -319,23 +407,76 @@ const CampaignList: React.FC<{
</header>
<section>
{/* Bulk Actions Bar */}
{selectedCampaigns.size > 0 && (
<div className="mb-4 bg-blue-50 border border-blue-200 rounded-lg p-3 flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-blue-800">
{selectedCampaigns.size} campaign{selectedCampaigns.size !== 1 ? 's' : ''} selected
</span>
<button
onClick={() => setSelectedCampaigns(new Set())}
className="text-sm text-blue-600 hover:text-blue-800 underline"
>
Clear selection
</button>
</div>
<button
onClick={() => setShowBulkDeleteModal(true)}
disabled={isDeleting}
className="flex items-center gap-2 bg-red-600 text-white font-semibold py-1.5 px-3 rounded-lg hover:bg-red-700 transition-colors duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
<TrashIcon className="h-4 w-4" />
Delete Selected
</button>
</div>
)}
<div className="bg-white rounded-lg shadow-md overflow-hidden border border-gray-200">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-4 py-3 text-left">
<input
type="checkbox"
checked={isAllSelected}
ref={(el) => {
if (el) el.indeterminate = isIndeterminate;
}}
onChange={handleSelectAll}
className="h-4 w-4 text-brand-accent border-gray-300 rounded focus:ring-brand-accent cursor-pointer"
aria-label="Select all campaigns"
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Campaign Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Proofs</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Created By</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Owning Agency</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Last Modified</th>
<th scope="col" className="relative px-4 py-3"><span className="sr-only">Actions</span></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredCampaigns.map((campaign) => {
const isSelected = selectedCampaigns.has(campaign.name);
return (
<tr key={campaign.name} className="hover:bg-gray-100 cursor-pointer" onClick={() => onSelectCampaign(campaign.name)}>
<tr
key={campaign.name}
className={`hover:bg-gray-100 cursor-pointer ${isSelected ? 'bg-blue-50' : ''}`}
onClick={() => onSelectCampaign(campaign.name)}
>
<td className="px-4 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={isSelected}
onChange={() => {}} // Handled by onClick
onClick={(e) => handleSelectCampaign(campaign.name, e)}
className="h-4 w-4 text-brand-accent border-gray-300 rounded focus:ring-brand-accent cursor-pointer"
aria-label={`Select ${campaign.name}`}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-brand-dark-blue">{campaign.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{campaign.proofs}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm w-40">
@ -356,6 +497,17 @@ const CampaignList: React.FC<{
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{campaign.agencyLead}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{campaign.agency}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{formatDate(campaign.lastModified)}</td>
<td className="px-4 py-4 whitespace-nowrap text-sm text-right">
<button
onClick={(e) => handleSingleDelete(campaign, e)}
disabled={isDeleting}
className="p-2 text-gray-400 rounded-full hover:bg-red-100 hover:text-red-600 transition-colors disabled:opacity-50"
title={`Delete ${campaign.name}`}
aria-label={`Delete ${campaign.name}`}
>
<TrashIcon className="h-5 w-5" />
</button>
</td>
</tr>
);
})}
@ -740,6 +892,57 @@ const DeleteConfirmationModal: React.FC<{
);
};
const CampaignDeleteConfirmationModal: React.FC<{
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
campaignName: string;
proofCount: number;
isBulk: boolean;
selectedCount: number;
}> = ({ isOpen, onClose, onConfirm, campaignName, proofCount, isBulk, selectedCount }) => {
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50 transition-opacity duration-300"
onClick={onClose}
aria-modal="true"
role="dialog"
>
<div
className="bg-white rounded-lg shadow-xl p-6 sm:p-8 w-full max-w-md transform transition-all"
onClick={e => e.stopPropagation()}
>
<h3 className="text-xl font-bold text-brand-dark-blue">Confirm Deletion</h3>
<p className="text-gray-600 my-4">
{isBulk ? (
<>Are you sure you want to permanently delete <strong>{selectedCount} selected campaign{selectedCount !== 1 ? 's' : ''}</strong>? All associated proofs will also be deleted. This action cannot be undone.</>
) : (
<>Are you sure you want to permanently delete the campaign "<strong>{campaignName}</strong>"? {proofCount > 0 && <>This will also delete <strong>{proofCount} proof{proofCount !== 1 ? 's' : ''}</strong>.</>} This action cannot be undone.</>
)}
</p>
<div className="mt-6 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="bg-gray-200 text-gray-800 font-semibold py-2 px-4 rounded-md hover:bg-gray-300 transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className="bg-red-600 text-white font-semibold py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>
{isBulk ? `Delete ${selectedCount} Campaign${selectedCount !== 1 ? 's' : ''}` : 'Delete Campaign'}
</button>
</div>
</div>
</div>
);
};
const CampaignDetail: React.FC<{
campaignName: string;
onBack: () => void;
@ -1398,16 +1601,17 @@ interface CampaignsProps {
onRetryAnalysis: (campaignName: string, tempId: string) => void;
onCampaignStatusChange: (campaignName: string, newStatus: 'In Progress' | 'Completed') => void;
onDeleteProof: (campaignName: string, proofName: string) => void;
onDeleteCampaign: (campaignName: string) => Promise<void>;
onFlagSubmit: (flagData: Omit<FlaggedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => void;
onResolveSubmit: (resolveData: Omit<ResolvedItem, 'id' | 'timestamp' | 'submitter' | 'submitAgency'>) => void;
}
export const Campaigns: React.FC<CampaignsProps> = ({
selectedCampaign,
selectedProof,
onSelectCampaign,
onSelectProof,
onBackToCampaignsList,
export const Campaigns: React.FC<CampaignsProps> = ({
selectedCampaign,
selectedProof,
onSelectCampaign,
onSelectProof,
onBackToCampaignsList,
onBackToCampaignDetails,
campaigns,
campaignProofs,
@ -1417,6 +1621,7 @@ export const Campaigns: React.FC<CampaignsProps> = ({
onRetryAnalysis,
onCampaignStatusChange,
onDeleteProof,
onDeleteCampaign,
onFlagSubmit,
onResolveSubmit,
}) => {
@ -1471,11 +1676,12 @@ export const Campaigns: React.FC<CampaignsProps> = ({
onAddCampaign={onAddNewCampaign}
brandGuidelines={dropdownOptions.brandGuidelines}
/>
<CampaignList
onSelectCampaign={onSelectCampaign}
<CampaignList
onSelectCampaign={onSelectCampaign}
campaigns={campaigns}
onOpenModal={() => setIsModalOpen(true)}
onCampaignStatusChange={onCampaignStatusChange}
onDeleteCampaign={(campaign) => onDeleteCampaign(campaign.name)}
/>
</>
);