Skip to content

Commit bddfe7a

Browse files
authored
feat: add delete reference functionality with confirmation modal (#908)
* feat: add delete reference functionality with confirmation modal * feat: refactor deleteReference to use transaction for cascading deletion * feat: reset delete modal state after successful deletion
1 parent 0ee2f6e commit bddfe7a

File tree

4 files changed

+141
-2
lines changed

4 files changed

+141
-2
lines changed

platforms/ereputation/api/src/controllers/ReferenceController.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,22 @@ export class ReferenceController {
252252
res.status(500).json({ error: "Internal server error" });
253253
}
254254
};
255+
256+
deleteReference = async (req: Request, res: Response) => {
257+
try {
258+
const { referenceId } = req.params;
259+
const userId = req.user!.id;
260+
261+
const deleted = await this.referenceService.deleteReference(referenceId, userId);
262+
263+
if (!deleted) {
264+
return res.status(404).json({ error: "Reference not found or not authorized" });
265+
}
266+
267+
res.json({ message: "Reference deleted successfully" });
268+
} catch (error) {
269+
console.error("Error deleting reference:", error);
270+
res.status(500).json({ error: "Internal server error" });
271+
}
272+
};
255273
}

platforms/ereputation/api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ app.get("/api/references/target/:targetType/:targetId", referenceController.getR
108108
app.get("/api/references/my", authGuard, referenceController.getUserReferences);
109109
app.get("/api/references", authGuard, referenceController.getAllUserReferences);
110110
app.patch("/api/references/:referenceId/revoke", authGuard, referenceController.revokeReference);
111+
app.delete("/api/references/:referenceId", authGuard, referenceController.deleteReference);
111112

112113
// Reference signing routes
113114
app.post("/api/references/signing/session", authGuard, referenceSigningController.createSigningSession.bind(referenceSigningController));

platforms/ereputation/api/src/services/ReferenceService.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,21 @@ export class ReferenceService {
9494
reference.status = "revoked";
9595
return await this.referenceRepository.save(reference);
9696
}
97+
98+
async deleteReference(referenceId: string, authorId: string): Promise<boolean> {
99+
const reference = await this.referenceRepository.findOne({
100+
where: { id: referenceId, authorId }
101+
});
102+
103+
if (!reference) {
104+
return false;
105+
}
106+
107+
await AppDataSource.manager.transaction(async (manager) => {
108+
await manager.delete("ReferenceSignature", { referenceId });
109+
await manager.remove(reference);
110+
});
111+
112+
return true;
113+
}
97114
}

platforms/ereputation/client/client/src/pages/dashboard.tsx

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useEffect, useState } from "react";
2-
import { useQuery } from "@tanstack/react-query";
2+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
33
import { useAuth } from "@/hooks/useAuth";
4-
import { clearAuth } from "@/lib/authUtils";
4+
import { clearAuth, isUnauthorizedError } from "@/lib/authUtils";
55
import { apiClient } from "@/lib/apiClient";
6+
import { useToast } from "@/hooks/use-toast";
67
import { Badge } from "@/components/ui/badge";
78
import { Button } from "@/components/ui/button";
89
import {
@@ -16,20 +17,24 @@ import {
1617
DialogContent,
1718
DialogHeader,
1819
DialogTitle,
20+
DialogDescription,
1921
} from "@/components/ui/dialog";
2022
import OtherCalculationModal from "@/components/modals/other-calculation-modal";
2123
import ReferenceModal from "@/components/modals/reference-modal";
2224
import ReferenceViewModal from "@/components/modals/reference-view-modal";
2325

2426
export default function Dashboard() {
2527
const { user, isAuthenticated, isLoading } = useAuth();
28+
const { toast } = useToast();
29+
const queryClient = useQueryClient();
2630
const [otherModalOpen, setOtherModalOpen] = useState(false);
2731
const [referenceModalOpen, setReferenceModalOpen] = useState(false);
2832
const [viewModalOpen, setViewModalOpen] = useState(false);
2933
const [selectedActivity, setSelectedActivity] = useState(null);
3034
const [referenceViewModal, setReferenceViewModal] = useState<any>(null);
3135
const [activeFilter, setActiveFilter] = useState<string>('all');
3236
const [currentPage, setCurrentPage] = useState(1);
37+
const [deleteModalOpen, setDeleteModalOpen] = useState<any>(null);
3338

3439
// This page is only rendered when authenticated, no need for redirect logic
3540

@@ -72,6 +77,48 @@ export default function Dashboard() {
7277
window.location.href = "/";
7378
};
7479

80+
// Delete reference mutation
81+
const deleteMutation = useMutation({
82+
mutationFn: async (referenceId: string) => {
83+
return await apiClient.delete(`/api/references/${referenceId}`);
84+
},
85+
onSuccess: () => {
86+
queryClient.invalidateQueries({ queryKey: ["/api/dashboard/activities"] });
87+
queryClient.invalidateQueries({ queryKey: ["/api/dashboard/stats"] });
88+
toast({
89+
title: "Reference Deleted",
90+
description: "The reference has been successfully deleted.",
91+
});
92+
setDeleteModalOpen(null);
93+
},
94+
onError: (error) => {
95+
if (isUnauthorizedError(error)) {
96+
toast({
97+
title: "Unauthorized",
98+
description: "You are logged out. Logging in again...",
99+
variant: "destructive",
100+
});
101+
setTimeout(() => {
102+
window.location.href = "/";
103+
}, 500);
104+
return;
105+
}
106+
toast({
107+
title: "Error",
108+
description: "Failed to delete reference. Please try again.",
109+
variant: "destructive",
110+
});
111+
},
112+
});
113+
114+
const confirmDeleteActivity = () => {
115+
if (deleteModalOpen) {
116+
// Activity IDs are prefixed (e.g. "ref-sent-<uuid>"), extract the actual reference UUID
117+
const referenceId = deleteModalOpen.id.replace(/^ref-(sent|received)-/, '');
118+
deleteMutation.mutate(referenceId);
119+
}
120+
};
121+
75122
const handleViewActivity = (activity: any) => {
76123
// For reference activities, show reference details modal
77124
if (activity.type === 'reference' || activity.activity === 'Reference Provided' || activity.activity === 'Reference Received') {
@@ -580,6 +627,17 @@ export default function Dashboard() {
580627
</svg>
581628
View Details
582629
</DropdownMenuItem>
630+
{activity.activity === 'Reference Provided' && (
631+
<DropdownMenuItem
632+
className="text-red-600"
633+
onClick={() => setDeleteModalOpen(activity)}
634+
>
635+
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
636+
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
637+
</svg>
638+
Delete
639+
</DropdownMenuItem>
640+
)}
583641
</DropdownMenuContent>
584642
</DropdownMenu>
585643
</td>
@@ -626,6 +684,17 @@ export default function Dashboard() {
626684
</svg>
627685
View Details
628686
</DropdownMenuItem>
687+
{activity.activity === 'Reference Provided' && (
688+
<DropdownMenuItem
689+
className="text-red-600"
690+
onClick={() => setDeleteModalOpen(activity)}
691+
>
692+
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
693+
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
694+
</svg>
695+
Delete
696+
</DropdownMenuItem>
697+
)}
629698
</DropdownMenuContent>
630699
</DropdownMenu>
631700
<div className="flex items-center gap-2">
@@ -915,6 +984,40 @@ export default function Dashboard() {
915984
) : null}
916985
</DialogContent>
917986
</Dialog>
987+
988+
{/* Delete Reference Confirmation Modal */}
989+
<Dialog open={!!deleteModalOpen} onOpenChange={(open) => !open && setDeleteModalOpen(null)}>
990+
<DialogContent className="w-full max-w-sm sm:max-w-md mx-4 sm:mx-auto bg-fig-10 border-2 border-fig/20 shadow-2xl rounded-xl">
991+
<DialogHeader className="text-center">
992+
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
993+
<svg className="w-8 h-8 text-red-600" fill="currentColor" viewBox="0 0 20 20">
994+
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
995+
</svg>
996+
</div>
997+
<DialogTitle className="text-xl font-black text-fig">Delete Reference</DialogTitle>
998+
<DialogDescription className="text-fig/70 text-sm font-medium mt-2">
999+
Are you sure you want to permanently delete the reference for <strong>{deleteModalOpen?.target}</strong>? This action cannot be undone.
1000+
</DialogDescription>
1001+
</DialogHeader>
1002+
1003+
<div className="flex gap-3 mt-6">
1004+
<Button
1005+
variant="outline"
1006+
onClick={() => setDeleteModalOpen(null)}
1007+
className="flex-1 border-2 border-fig/30 text-fig/70 hover:bg-fig-10 hover:border-fig/40 font-bold h-11"
1008+
>
1009+
Cancel
1010+
</Button>
1011+
<Button
1012+
onClick={confirmDeleteActivity}
1013+
disabled={deleteMutation.isPending}
1014+
className="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold h-11"
1015+
>
1016+
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
1017+
</Button>
1018+
</div>
1019+
</DialogContent>
1020+
</Dialog>
9181021
</div>
9191022
);
9201023
}

0 commit comments

Comments
 (0)