From 573785e440ee3a6eb5aabd8c31de43e4b70c8a72 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 10 Feb 2026 20:42:33 +0100 Subject: [PATCH] Fix round deletion FK constraint with migration and defensive code - Add SQL migration to CASCADE Evaluation.formId and SET NULL ProjectFile.roundId - Explicitly delete evaluations in round delete transaction as defensive measure - Make sidebar Apply Page link dynamic using current edition context Co-Authored-By: Claude Opus 4.6 --- .../migration.sql | 13 +++++++++++++ src/components/layouts/admin-sidebar.tsx | 12 +++++++++++- src/server/routers/round.ts | 6 ++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20260210000000_fix_round_delete_cascades/migration.sql diff --git a/prisma/migrations/20260210000000_fix_round_delete_cascades/migration.sql b/prisma/migrations/20260210000000_fix_round_delete_cascades/migration.sql new file mode 100644 index 0000000..2461453 --- /dev/null +++ b/prisma/migrations/20260210000000_fix_round_delete_cascades/migration.sql @@ -0,0 +1,13 @@ +-- Fix round deletion FK constraint errors +-- Add CASCADE on Evaluation.formId so deleting EvaluationForm cascades to Evaluations +-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference + +-- AlterTable: Evaluation.formId -> onDelete CASCADE +ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey"; +ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey" + FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AlterTable: ProjectFile.roundId -> onDelete SET NULL +ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey"; +ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey" + FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/components/layouts/admin-sidebar.tsx b/src/components/layouts/admin-sidebar.tsx index 308db07..e7217d6 100644 --- a/src/components/layouts/admin-sidebar.tsx +++ b/src/components/layouts/admin-sidebar.tsx @@ -38,6 +38,7 @@ import { import { getInitials } from '@/lib/utils' import { Logo } from '@/components/shared/logo' import { EditionSelector } from '@/components/shared/edition-selector' +import { useEdition } from '@/contexts/edition-context' import { UserAvatar } from '@/components/shared/user-avatar' import { NotificationBell } from '@/components/shared/notification-bell' import { trpc } from '@/lib/trpc/client' @@ -145,10 +146,19 @@ export function AdminSidebar({ user }: AdminSidebarProps) { const pathname = usePathname() const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) const { data: avatarUrl } = trpc.avatar.getUrl.useQuery() + const { currentEdition } = useEdition() const isSuperAdmin = user.role === 'SUPER_ADMIN' const roleLabel = roleLabels[user.role || ''] || 'User' + // Build dynamic admin nav with current edition's apply page + const dynamicAdminNav = adminNavigation.map((item) => { + if (item.name === 'Apply Page' && currentEdition?.id) { + return { ...item, href: `/admin/programs/${currentEdition.id}/apply-settings` } + } + return item + }) + return ( <> {/* Mobile menu button */} @@ -235,7 +245,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) {

Administration

- {adminNavigation.map((item) => { + {dynamicAdminNav.map((item) => { let isActive = pathname.startsWith(item.href) if (item.activeMatch) { isActive = pathname.includes(item.activeMatch) diff --git a/src/server/routers/round.ts b/src/server/routers/round.ts index 818dc1c..0b9147a 100644 --- a/src/server/routers/round.ts +++ b/src/server/routers/round.ts @@ -615,6 +615,12 @@ export const roundRouter = router({ userAgent: ctx.userAgent, }) + // Delete evaluations first to avoid FK constraint on Evaluation.formId + // (formId FK may not have CASCADE in older DB schemas) + await tx.evaluation.deleteMany({ + where: { form: { roundId: input.id } }, + }) + await tx.round.delete({ where: { id: input.id }, })