Add schema reconciliation migration and file removal in bulk upload

Migration:
- Add standalone hasConflict index on ConflictOfInterest
- Ensure roundId is nullable on ConflictOfInterest
- Drop stale composite roundId_hasConflict index

Bulk upload:
- Add trash icon button to remove uploaded files
- Uses existing file.delete endpoint with audit logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-16 13:46:12 +01:00
parent 85a0fa5016
commit 5e0c8b2dfe
2 changed files with 54 additions and 1 deletions

View File

@ -0,0 +1,19 @@
-- =============================================================================
-- Schema Reconciliation: Fill remaining gaps between migrations and schema.prisma
-- =============================================================================
-- All statements are idempotent (safe to re-run on any database state).
-- 1. ConflictOfInterest: add standalone hasConflict index (schema has @@index([hasConflict]))
-- Migration 20260205223133 only created composite (roundId, hasConflict) index.
CREATE INDEX IF NOT EXISTS "ConflictOfInterest_hasConflict_idx" ON "ConflictOfInterest"("hasConflict");
-- 2. Ensure ConflictOfInterest.roundId is nullable (schema says String?)
-- Pipeline migration (20260213) makes it nullable, but guard for safety.
DO $$ BEGIN
ALTER TABLE "ConflictOfInterest" ALTER COLUMN "roundId" DROP NOT NULL;
EXCEPTION WHEN others THEN NULL;
END $$;
-- 3. Drop stale composite index that no longer matches schema
-- Schema only has @@index([hasConflict]) and @@index([userId]), not (roundId, hasConflict).
DROP INDEX IF EXISTS "ConflictOfInterest_roundId_hasConflict_idx";

View File

@ -47,6 +47,7 @@ import {
FileUp, FileUp,
AlertCircle, AlertCircle,
ExternalLink, ExternalLink,
Trash2,
} from 'lucide-react' } from 'lucide-react'
import { cn, formatFileSize } from '@/lib/utils' import { cn, formatFileSize } from '@/lib/utils'
import { Pagination } from '@/components/shared/pagination' import { Pagination } from '@/components/shared/pagination'
@ -155,6 +156,26 @@ export default function BulkUploadPage() {
[utils, refetch] [utils, refetch]
) )
// Delete a file
const deleteMutation = trpc.file.delete.useMutation({
onSuccess: () => {
toast.success('File removed')
refetch()
},
onError: (err) => {
toast.error(`Failed to remove file: ${err.message}`)
},
})
const handleDeleteFile = useCallback(
(fileId: string) => {
if (confirm('Remove this uploaded file?')) {
deleteMutation.mutate({ id: fileId })
}
},
[deleteMutation]
)
const uploadMutation = trpc.file.adminUploadForRoundRequirement.useMutation() const uploadMutation = trpc.file.adminUploadForRoundRequirement.useMutation()
// Upload a single file for a project requirement // Upload a single file for a project requirement
@ -513,7 +534,20 @@ export default function BulkUploadPage() {
</div> </div>
) : req.file || uploadState?.status === 'complete' ? ( ) : req.file || uploadState?.status === 'complete' ? (
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<CheckCircle2 className="h-4 w-4 text-green-600" /> <div className="flex items-center gap-1">
<CheckCircle2 className="h-4 w-4 text-green-600" />
{req.file && (
<button
type="button"
className="text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
title="Remove file"
onClick={() => handleDeleteFile(req.file!.id)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-3 w-3" />
</button>
)}
</div>
{req.file?.bucket && req.file?.objectKey ? ( {req.file?.bucket && req.file?.objectKey ? (
<button <button
type="button" type="button"