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:
parent
5388c390ed
commit
687edb547c
2 changed files with 242 additions and 12 deletions
|
|
@ -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}
|
||||
/>;
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue