From 005f7651f29d32cbf71e09d926225924d9b1cd5b Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Thu, 1 Jan 2026 21:12:58 -0500 Subject: [PATCH 1/9] #3844 ruleset table has data --- .../RulesPage/components/RulesetTable.tsx | 228 ++++-------------- .../RulesPage/components/RulesetTypeTable.tsx | 6 +- 2 files changed, 44 insertions(+), 190 deletions(-) diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index db99b85dc3..0c970bc946 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -18,6 +18,11 @@ import { } from '@mui/material'; import { datePipe } from '../../../utils/pipes'; import { NERButton } from '../../../components/NERButton'; +import { useHistory, useParams } from 'react-router-dom'; +import LoadingIndicator from '../../../components/LoadingIndicator'; +import ErrorPage from '../../ErrorPage'; +import { useRulesetsByType } from '../../../hooks/rules.hooks'; +import { Ruleset } from 'shared'; interface RulesetRow { id: string; @@ -28,13 +33,21 @@ interface RulesetRow { isActive: boolean; } +interface RulesetParams { + rulesetId: string; +} + const RulesetTable: React.FC = () => { + const { rulesetId } = useParams(); + const history = useHistory(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); // Add file upload logic // const [AddFileModalShow, setAddFileModalShow] = React.useState(false); + const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetId); + // Table header configuration const headCells = [ { id: 'fileName', label: 'File Name' }, @@ -45,185 +58,26 @@ const RulesetTable: React.FC = () => { { id: 'actions', label: 'Actions' } ]; - // Mock data for now - will be replaced with ruleset data - const mockRulesets: RulesetRow[] = [ - { - id: '1', - fileName: 'FSAE Original Version', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '2', - fileName: 'FSAE Revision 1', - dateUploaded: new Date('2025-02-25'), - percentRulesAssigned: 10, - car: 1, - isActive: true - }, - { - id: '3', - fileName: 'Hi', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - }, - { - id: '3', - fileName: 'Hi ', - dateUploaded: new Date('2025-02-24'), - percentRulesAssigned: 80, - car: 1, - isActive: false - } - ]; + const handleEditRuleset = () => { + // Navigation logic to edit/assign rules pages + history.push(`/rules/${rulesetId}/edit`); + }; + + const handleViewRuleset = () => { + // Navigation logic to view rules pages + history.push(`/rules/${rulesetId}/view`); + }; + + if (isLoading) return ; + if (error) return ; return ( {isMobile ? ( - {mockRulesets.map((ruleset) => ( + {rulesets.map((ruleset: Ruleset) => ( { > - {ruleset.fileName} + {ruleset.name} @@ -240,7 +94,7 @@ const RulesetTable: React.FC = () => { Date Uploaded: - {datePipe(ruleset.dateUploaded)} + {datePipe(ruleset.dateCreated)} @@ -248,7 +102,7 @@ const RulesetTable: React.FC = () => { % of Rules Assigned: - {ruleset.percentRulesAssigned} + {ruleset.assignedPercentage} @@ -256,7 +110,7 @@ const RulesetTable: React.FC = () => { Car: - {ruleset.car} + {ruleset.car.carId} @@ -264,10 +118,10 @@ const RulesetTable: React.FC = () => { Active: - {ruleset.isActive} + {ruleset.active} { {/* Table rows with ruleset data */} - {mockRulesets.length === 0 ? ( + {rulesets.length === 0 ? ( No Rulesets Found ) : ( - mockRulesets.map((ruleset) => ( + rulesets.map((ruleset: Ruleset) => ( - {ruleset.fileName} + {ruleset.name} - {datePipe(ruleset.dateUploaded)} - {ruleset.percentRulesAssigned}% - {ruleset.car} + {datePipe(ruleset.dateCreated)} + {ruleset.assignedPercentage}% + {ruleset.car.carId} { } ]; - const handleViewRuleset = (rulesetTypeId: string) => { + const handleViewRulesetType = (rulesetTypeId: string) => { history.push(routes.RULESET_BY_ID.replace(':rulesetId', rulesetTypeId)); }; @@ -111,7 +111,7 @@ const RulesetTypeTable: React.FC = () => { lineHeight: 1, borderRadius: '6px' }} - onClick={() => handleViewRuleset(rulesetType.rulesetTypeId)} + onClick={() => handleViewRulesetType(rulesetType.rulesetTypeId)} > View Ruleset @@ -193,7 +193,7 @@ const RulesetTypeTable: React.FC = () => { lineHeight: 1, borderRadius: '6px' }} - onClick={() => handleViewRuleset(rulesetType.rulesetTypeId)} + onClick={() => handleViewRulesetType(rulesetType.rulesetTypeId)} > View Ruleset From 4b62a383bdea92afc3ed34de02814c9ab5f02995 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Thu, 1 Jan 2026 22:21:34 -0500 Subject: [PATCH 2/9] #3844 buttons functional --- src/backend/src/services/rules.services.ts | 4 +-- .../src/pages/RulesPage/RulesetPage.tsx | 3 -- .../RulesPage/components/RulesetTable.tsx | 30 ++++++++----------- .../RulesPage/components/RulesetTypeTable.tsx | 2 +- src/frontend/src/utils/routes.ts | 6 ++-- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index bab0c0aa89..1c9016e1ba 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -525,10 +525,10 @@ export default class RulesService { organizationId } }, - ...getRulesetPreviewQueryArgs() + ...getRulesetQueryArgs(organizationId) }); - return rulesets.map(rulesetPreviewTransformer); + return rulesets.map(rulesetTransformer); } /** diff --git a/src/frontend/src/pages/RulesPage/RulesetPage.tsx b/src/frontend/src/pages/RulesPage/RulesetPage.tsx index 84cd954c5a..9f328213aa 100644 --- a/src/frontend/src/pages/RulesPage/RulesetPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetPage.tsx @@ -67,9 +67,6 @@ const RulesetPage: React.FC = () => { onConfirm={handleFileConfirm} carOptions={['1', '2']} /> - history.push(`${routes.RULES}/placeholder_ruleset_id/edit`)}> - MOCK edit/assign rules - diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index 0c970bc946..70c141584d 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -23,22 +23,14 @@ import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { useRulesetsByType } from '../../../hooks/rules.hooks'; import { Ruleset } from 'shared'; - -interface RulesetRow { - id: string; - fileName: string; - dateUploaded: Date; - percentRulesAssigned: number; - car: number; - isActive: boolean; -} +import { routes } from '../../../utils/routes'; interface RulesetParams { - rulesetId: string; + rulesetTypeId: string; } const RulesetTable: React.FC = () => { - const { rulesetId } = useParams(); + const { rulesetTypeId } = useParams(); const history = useHistory(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -46,7 +38,7 @@ const RulesetTable: React.FC = () => { // Add file upload logic // const [AddFileModalShow, setAddFileModalShow] = React.useState(false); - const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetId); + const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetTypeId); // Table header configuration const headCells = [ @@ -58,14 +50,12 @@ const RulesetTable: React.FC = () => { { id: 'actions', label: 'Actions' } ]; - const handleEditRuleset = () => { - // Navigation logic to edit/assign rules pages - history.push(`/rules/${rulesetId}/edit`); + const handleEditRuleset = (rulesetId: string) => { + history.push(routes.RULESET_EDIT.replace(':rulesetId', rulesetId)); }; - const handleViewRuleset = () => { - // Navigation logic to view rules pages - history.push(`/rules/${rulesetId}/view`); + const handleViewRuleset = (rulesetId: string) => { + history.push(routes.RULESET_VIEW.replace(':rulesetId', rulesetId)); }; if (isLoading) return ; @@ -131,6 +121,7 @@ const RulesetTable: React.FC = () => { handleEditRuleset(ruleset.rulesetId)} sx={{ backgroundColor: theme.palette.grey[800], color: theme.palette.getContrastText(theme.palette.grey[600]), @@ -146,6 +137,7 @@ const RulesetTable: React.FC = () => { Edit/Assign Rules handleViewRuleset(ruleset.rulesetId)} sx={{ backgroundColor: theme.palette.grey[800], color: theme.palette.getContrastText(theme.palette.grey[600]), @@ -216,6 +208,7 @@ const RulesetTable: React.FC = () => { handleEditRuleset(ruleset.rulesetId)} sx={{ backgroundColor: theme.palette.grey[800], color: theme.palette.getContrastText(theme.palette.grey[600]), @@ -231,6 +224,7 @@ const RulesetTable: React.FC = () => { Edit/Assign Rules handleViewRuleset(ruleset.rulesetId)} sx={{ backgroundColor: theme.palette.grey[800], color: theme.palette.getContrastText(theme.palette.grey[600]), diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx index 6bd774024b..7069583bb3 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx @@ -58,7 +58,7 @@ const RulesetTypeTable: React.FC = () => { ]; const handleViewRulesetType = (rulesetTypeId: string) => { - history.push(routes.RULESET_BY_ID.replace(':rulesetId', rulesetTypeId)); + history.push(routes.RULESET_BY_ID.replace(':rulesetTypeId', rulesetTypeId)); }; if (isLoading) return ; diff --git a/src/frontend/src/utils/routes.ts b/src/frontend/src/utils/routes.ts index aa6ce0de87..0a54289a25 100644 --- a/src/frontend/src/utils/routes.ts +++ b/src/frontend/src/utils/routes.ts @@ -82,9 +82,9 @@ const RETROSPECTIVE = `/retrospective`; /**************** Rules ****************/ const RULES = `/rules`; -const RULESET_BY_ID = RULES + `/:rulesetId`; -const RULESET_VIEW = RULESET_BY_ID + `/view`; -const RULESET_EDIT = RULESET_BY_ID + `/edit`; +const RULESET_BY_ID = RULES + `/:rulesetTypeId`; +const RULESET_VIEW = RULES + `/ruleset/:rulesetId/view`; +const RULESET_EDIT = RULES + `/ruleset/:rulesetId/edit`; export const routes = { BASE, From 72503cb48a39f4b7f0b7e158396574710b56dea3 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Fri, 2 Jan 2026 12:16:56 -0500 Subject: [PATCH 3/9] #3844 linting/prettier and car id fix + ruleset display fix --- .../src/controllers/rules.controllers.ts | 2 +- .../src/prisma-query-args/rules.query-args.ts | 17 +++-------------- src/backend/src/services/rules.services.ts | 10 ++++------ .../src/transformers/rules.transformer.ts | 19 ++----------------- .../src/pages/RulesPage/RulesetPage.tsx | 2 -- .../RulesPage/components/RulesetTable.tsx | 4 ++-- .../RulesPage/components/RulesetTypeTable.tsx | 4 ++-- src/frontend/src/utils/routes.ts | 2 +- 8 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/backend/src/controllers/rules.controllers.ts b/src/backend/src/controllers/rules.controllers.ts index 500c400625..e1e15624fc 100644 --- a/src/backend/src/controllers/rules.controllers.ts +++ b/src/backend/src/controllers/rules.controllers.ts @@ -101,7 +101,7 @@ export default class RulesController { static async getRulesetsByRulesetType(req: Request, res: Response, next: NextFunction) { try { - const rulesetTypeId = req.body; + const { rulesetTypeId } = req.params; const rulesets = await RulesService.getRulesetsByRulesetType(rulesetTypeId, req.organization.organizationId); res.status(200).json(rulesets); } catch (error: unknown) { diff --git a/src/backend/src/prisma-query-args/rules.query-args.ts b/src/backend/src/prisma-query-args/rules.query-args.ts index acb098e426..1a539fc951 100644 --- a/src/backend/src/prisma-query-args/rules.query-args.ts +++ b/src/backend/src/prisma-query-args/rules.query-args.ts @@ -65,22 +65,11 @@ export const getRulesetQueryArgs = (organizationId: string) => } }, rulesetType: true, - car: true, - createdBy: getUserQueryArgs(organizationId) - } - }); - -export const getRulesetPreviewQueryArgs = () => - Prisma.validator()({ - select: { - name: true, - dateCreated: true, - rulesetType: true, - active: true, car: { - select: { + include: { wbsElement: true } - } + }, + createdBy: getUserQueryArgs(organizationId) } }); diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index 1c9016e1ba..6a3d9e7232 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -5,10 +5,10 @@ import { ProjectRule, RulesetType, notGuest, - RulesetPreview, User, Rule as SharedRule, - isHead + isHead, + Ruleset } from 'shared'; import prisma from '../prisma/prisma'; import { @@ -24,15 +24,13 @@ import { userHasPermission } from '../utils/users.utils'; import { getProjectRuleQueryArgs, getRulesetQueryArgs, - getRulesetPreviewQueryArgs, getRulePreviewQueryArgs } from '../prisma-query-args/rules.query-args'; import { ruleTransformer, projectRuleTransformer, rulesetTransformer, - rulesetTypeTransformer, - rulesetPreviewTransformer + rulesetTypeTransformer } from '../transformers/rules.transformer'; export default class RulesService { @@ -516,7 +514,7 @@ export default class RulesService { * @param organizationId id of organization * @returns rulesets associated with provided ruleset type */ - static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string): Promise { + static async getRulesetsByRulesetType(rulesetTypeId: string, organizationId: string): Promise { const rulesets = await prisma.ruleset.findMany({ where: { rulesetTypeId, diff --git a/src/backend/src/transformers/rules.transformer.ts b/src/backend/src/transformers/rules.transformer.ts index 4c1b0af6be..8b3301c4be 100644 --- a/src/backend/src/transformers/rules.transformer.ts +++ b/src/backend/src/transformers/rules.transformer.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { Rule, ProjectRule, Ruleset, RulesetType, RulesetPreview } from 'shared'; +import { Rule, ProjectRule, Ruleset, RulesetType } from 'shared'; import { RulesetQueryArgs, RulePreviewQueryArgs } from '../prisma-query-args/rules.query-args'; export const ruleTransformer = (rule: Prisma.RuleGetPayload): Rule => { @@ -53,22 +53,7 @@ export const rulesetTransformer = (ruleset: Prisma.RulesetGetPayload { - const teamsPercentage = 0; - - return { - name: ruleset.name, - dateCreated: ruleset.dateCreated, - active: ruleset.active, - assignedPercentage: teamsPercentage, - car: { - carId: ruleset.car.carId, - name: ruleset.car.wbsElementId + name: ruleset.car.wbsElement.name } }; }; diff --git a/src/frontend/src/pages/RulesPage/RulesetPage.tsx b/src/frontend/src/pages/RulesPage/RulesetPage.tsx index 9f328213aa..e3461447a5 100644 --- a/src/frontend/src/pages/RulesPage/RulesetPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetPage.tsx @@ -3,7 +3,6 @@ * See the LICENSE file in the repository root folder for details. */ import { useHistory } from 'react-router-dom'; -import { routes } from '../../utils/routes'; import React from 'react'; import { NERButton } from '../../components/NERButton'; import AddNewFileModal from './components/AddNewFileModal'; @@ -16,7 +15,6 @@ import RulesetTable from './components/RulesetTable'; * Supports editing and assigning rules to projects and teams. */ const RulesetPage: React.FC = () => { - const history = useHistory(); const [AddFileModalShow, setAddFileModalShow] = React.useState(false); const handleFileConfirm = async (data: { file: File; name: string; car: string; isActive: boolean }) => { diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index 70c141584d..0c58b3d65e 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -100,7 +100,7 @@ const RulesetTable: React.FC = () => { Car: - {ruleset.car.carId} + {ruleset.car.name} @@ -195,7 +195,7 @@ const RulesetTable: React.FC = () => { {datePipe(ruleset.dateCreated)} {ruleset.assignedPercentage}% - {ruleset.car.carId} + {ruleset.car.name} { }} onClick={() => handleViewRulesetType(rulesetType.rulesetTypeId)} > - View Ruleset + View Rulesets @@ -195,7 +195,7 @@ const RulesetTypeTable: React.FC = () => { }} onClick={() => handleViewRulesetType(rulesetType.rulesetTypeId)} > - View Ruleset + View Rulesets diff --git a/src/frontend/src/utils/routes.ts b/src/frontend/src/utils/routes.ts index 0a54289a25..72dcfcab3d 100644 --- a/src/frontend/src/utils/routes.ts +++ b/src/frontend/src/utils/routes.ts @@ -83,7 +83,7 @@ const RETROSPECTIVE = `/retrospective`; /**************** Rules ****************/ const RULES = `/rules`; const RULESET_BY_ID = RULES + `/:rulesetTypeId`; -const RULESET_VIEW = RULES + `/ruleset/:rulesetId/view`; +const RULESET_VIEW = RULES + `/ruleset/:rulesetId/view`; const RULESET_EDIT = RULES + `/ruleset/:rulesetId/edit`; export const routes = { From 3da7f30a8cc8ba5259fecee86b67d492f6c64658 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Fri, 2 Jan 2026 12:20:46 -0500 Subject: [PATCH 4/9] #3844 lint --- src/frontend/src/pages/RulesPage/RulesetPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/pages/RulesPage/RulesetPage.tsx b/src/frontend/src/pages/RulesPage/RulesetPage.tsx index e3461447a5..123f9a765c 100644 --- a/src/frontend/src/pages/RulesPage/RulesetPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetPage.tsx @@ -2,7 +2,6 @@ * This file is part of NER's FinishLine and licensed under GNU AGPLv3. * See the LICENSE file in the repository root folder for details. */ -import { useHistory } from 'react-router-dom'; import React from 'react'; import { NERButton } from '../../components/NERButton'; import AddNewFileModal from './components/AddNewFileModal'; From af69f4e83b3b5afbdfd93fca977d66797bc850d8 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Fri, 2 Jan 2026 23:09:48 -0500 Subject: [PATCH 5/9] #3844 url + query fixes --- .../src/prisma-query-args/rules.query-args.ts | 6 ++---- src/backend/src/services/rules.services.ts | 18 +++++++++--------- .../src/pages/RulesPage/AssignRulesTab.tsx | 2 +- .../src/pages/RulesPage/RulesetEditPage.tsx | 2 +- .../src/pages/RulesPage/RulesetViewPage.tsx | 3 +-- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/backend/src/prisma-query-args/rules.query-args.ts b/src/backend/src/prisma-query-args/rules.query-args.ts index 1a539fc951..aa3cf262bd 100644 --- a/src/backend/src/prisma-query-args/rules.query-args.ts +++ b/src/backend/src/prisma-query-args/rules.query-args.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client'; -import { getUserQueryArgs } from './user.query-args'; export type RulePreviewQueryArgs = ReturnType; @@ -50,7 +49,7 @@ export const getProjectRuleQueryArgs = () => export type RulesetQueryArgs = ReturnType; -export const getRulesetQueryArgs = (organizationId: string) => +export const getRulesetQueryArgs = () => Prisma.validator()({ include: { rules: { @@ -69,7 +68,6 @@ export const getRulesetQueryArgs = (organizationId: string) => include: { wbsElement: true } - }, - createdBy: getUserQueryArgs(organizationId) + } } }); diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index 6a3d9e7232..73998ca3c0 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -59,7 +59,7 @@ export default class RulesService { const activeRuleset = await prisma.ruleset.findFirst({ where: { rulesetTypeId, deletedByUserId: null, active: true }, - ...getRulesetQueryArgs(organization.organizationId) + ...getRulesetQueryArgs() }); if (!activeRuleset) { @@ -459,10 +459,10 @@ export default class RulesService { * @param organizationId the id of the organization the ruleset is being deleted in * @returns the ruleset with query args */ - static async getRulesetWithQueryArgs(rulesetId: string, organizationId: string) { + static async getRulesetWithQueryArgs(rulesetId: string) { const ruleset = await prisma.ruleset.findUnique({ where: { rulesetId }, - ...getRulesetQueryArgs(organizationId) + ...getRulesetQueryArgs() }); if (!ruleset) throw new NotFoundException('Ruleset', rulesetId); @@ -479,7 +479,7 @@ export default class RulesService { * @returns the deleted Ruleset */ static async deleteRuleset(rulesetId: string, deleterId: string, organizationId: string) { - const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId, organizationId); + const ruleset = await RulesService.getRulesetWithQueryArgs(rulesetId); const hasPermission = (await userHasPermission(deleterId, organizationId, isAdmin)) || deleterId === ruleset.createdByUserId; @@ -489,7 +489,7 @@ export default class RulesService { const deletedRuleset = await prisma.ruleset.update({ where: { rulesetId }, data: { deletedBy: { connect: { userId: deleterId } } }, - ...getRulesetQueryArgs(organizationId) + ...getRulesetQueryArgs() }); return rulesetTransformer(deletedRuleset); @@ -523,7 +523,7 @@ export default class RulesService { organizationId } }, - ...getRulesetQueryArgs(organizationId) + ...getRulesetQueryArgs() }); return rulesets.map(rulesetTransformer); @@ -571,7 +571,7 @@ export default class RulesService { active: true, deletedBy: null }, - ...getRulesetQueryArgs(organization.organizationId) + ...getRulesetQueryArgs() }); if (!activeRuleset) { @@ -782,7 +782,7 @@ export default class RulesService { active, createdByUserId: submitter.userId }, - ...getRulesetQueryArgs(organization.organizationId) + ...getRulesetQueryArgs() }); return rulesetTransformer(ruleset); @@ -959,7 +959,7 @@ export default class RulesService { name, active: isActive }, - ...getRulesetQueryArgs(organizationId) + ...getRulesetQueryArgs() }); return rulesetTransformer(ruleset); diff --git a/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx b/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx index fa80b96bec..a3d3972464 100644 --- a/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx +++ b/src/frontend/src/pages/RulesPage/AssignRulesTab.tsx @@ -185,7 +185,7 @@ const AssignRulesTab: React.FC = ({ rules }) => { toast.success(`Placeholder: Would save ${toAdd.length} additions and ${toRemove.length} removals`); } - history.push(`${routes.RULES}/${rulesetId}`); + history.push(routes.RULESET_EDIT.replace(':rulesetId', rulesetId)); }; if (teamsLoading) { diff --git a/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx index 5ae6b638cf..2988a5e494 100644 --- a/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetEditPage.tsx @@ -240,7 +240,7 @@ const RulesetEditPage: React.FC = () => { diff --git a/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx index 85c9baf809..65b54d1bf6 100644 --- a/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx +++ b/src/frontend/src/pages/RulesPage/RulesetViewPage.tsx @@ -17,7 +17,6 @@ const RulesetViewPage = () => { ]; const { rulesetId } = useParams<{ rulesetId: string }>(); - const { data: ruleset, isError, error, isLoading } = useSingleRuleset(rulesetId); if (isError) { @@ -38,7 +37,7 @@ const RulesetViewPage = () => { noUnderline setTab={setTabIndex} tabsLabels={tabs} - baseUrl={`${routes.RULES}/${rulesetId}/view`} + baseUrl={routes.RULESET_VIEW.replace(':rulesetId', rulesetId)} defaultTab={'teamView'} id="rules-view-tabs" /> From a8422de3364bc07501d4f0564bf973dd9e5cda8b Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Fri, 2 Jan 2026 23:24:54 -0500 Subject: [PATCH 6/9] #3844 active checkbox works --- src/backend/src/services/rules.services.ts | 3 ++ src/frontend/src/apis/rules.api.ts | 7 +++++ src/frontend/src/hooks/rules.hooks.ts | 18 ++++++++++- .../RulesPage/components/RulesetTable.tsx | 30 +++++++++++++++++-- src/frontend/src/utils/urls.ts | 2 ++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index 73998ca3c0..2bbabb2e04 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -523,6 +523,9 @@ export default class RulesService { organizationId } }, + orderBy: { + dateCreated: 'desc' + }, ...getRulesetQueryArgs() }); diff --git a/src/frontend/src/apis/rules.api.ts b/src/frontend/src/apis/rules.api.ts index 8f90276370..8cdd80e7cc 100644 --- a/src/frontend/src/apis/rules.api.ts +++ b/src/frontend/src/apis/rules.api.ts @@ -67,3 +67,10 @@ export const getRulesetsByRulesetType = (rulesetTypeId: string) => { transformResponse: (data) => JSON.parse(data) }); }; + +/** + * Updates a rulesets active status + */ +export const updateRuleset = (rulesetId: string, name: string, isActive: boolean) => { + return axios.post(apiUrls.rulesetUpdate(rulesetId), { name, isActive }); +}; diff --git a/src/frontend/src/hooks/rules.hooks.ts b/src/frontend/src/hooks/rules.hooks.ts index 1183fe2384..6825d64f5b 100644 --- a/src/frontend/src/hooks/rules.hooks.ts +++ b/src/frontend/src/hooks/rules.hooks.ts @@ -12,7 +12,8 @@ import { getTeamRulesInRulesetType, createRulesetType, getAllRulesetTypes, - getRulesetsByRulesetType + getRulesetsByRulesetType, + updateRuleset } from '../apis/rules.api'; interface CreateRulesetTypePayload { @@ -99,3 +100,18 @@ export const useRulesetsByType = (rulesetTypeId: string) => { return data; }); }; + +export const useUpdateRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + async ({ rulesetId, name, isActive }) => { + const { data } = await updateRuleset(rulesetId, name, isActive); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index 0c58b3d65e..3220167d43 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -21,9 +21,10 @@ import { NERButton } from '../../../components/NERButton'; import { useHistory, useParams } from 'react-router-dom'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; -import { useRulesetsByType } from '../../../hooks/rules.hooks'; +import { useRulesetsByType, useUpdateRuleset } from '../../../hooks/rules.hooks'; import { Ruleset } from 'shared'; import { routes } from '../../../utils/routes'; +import { useToast } from '../../../hooks/toasts.hooks'; interface RulesetParams { rulesetTypeId: string; @@ -31,6 +32,7 @@ interface RulesetParams { const RulesetTable: React.FC = () => { const { rulesetTypeId } = useParams(); + const toast = useToast(); const history = useHistory(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -39,6 +41,7 @@ const RulesetTable: React.FC = () => { // const [AddFileModalShow, setAddFileModalShow] = React.useState(false); const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetTypeId); + const updateRuleset = useUpdateRuleset(); // Table header configuration const headCells = [ @@ -50,6 +53,25 @@ const RulesetTable: React.FC = () => { { id: 'actions', label: 'Actions' } ]; + const handleToggleActive = (ruleset: Ruleset) => { + updateRuleset.mutate( + { + rulesetId: ruleset.rulesetId, + name: ruleset.name, + isActive: !ruleset.active + }, + { + onSuccess: () => { + toast.success(ruleset.active ? 'Ruleset deactivated' : 'Ruleset activated'); + }, + onError: (error: any) => { + const message = error.response?.data?.message || error.message; + toast.error(message); + } + } + ); + }; + const handleEditRuleset = (rulesetId: string) => { history.push(routes.RULESET_EDIT.replace(':rulesetId', rulesetId)); }; @@ -112,7 +134,8 @@ const RulesetTable: React.FC = () => { handleToggleActive(ruleset)} + disabled={updateRuleset.isLoading} sx={{ color: '#fff', '&.Mui-checked': { color: '#dd514c' } @@ -199,7 +222,8 @@ const RulesetTable: React.FC = () => { handleToggleActive(ruleset)} + disabled={updateRuleset.isLoading} sx={{ color: '#fff', '&.Mui-checked': { color: '#dd514c' } diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 6cfd301a4f..68f7353af8 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -447,6 +447,7 @@ const rulesetsByType = (rulesetTypeId: string) => `${rules()}/rulesets/${ruleset const ruleset = () => `${rules()}/ruleset`; const rulesetTypeCreate = () => `${rules()}/rulesetType/create`; const rulesetsCreate = () => `${ruleset()}/create`; +const rulesetUpdate = (rulesetId: string) => `${ruleset()}/${rulesetId}/update`; /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -760,6 +761,7 @@ export const apiUrls = { rulesetsByType, rulesetTypeCreate, rulesetsCreate, + rulesetUpdate, version }; From becdcfaba79fcaa7e96e1f91209e7b63195f1e13 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Fri, 2 Jan 2026 23:30:02 -0500 Subject: [PATCH 7/9] #3844 ruleset ordering test fix --- src/backend/tests/unit/rule.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/tests/unit/rule.test.ts b/src/backend/tests/unit/rule.test.ts index 290733874f..93247bb4d2 100644 --- a/src/backend/tests/unit/rule.test.ts +++ b/src/backend/tests/unit/rule.test.ts @@ -395,8 +395,8 @@ describe('Create Rules Tests', () => { }); const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); expect(rulesets.length).toBe(2); - expect(rulesets[0].name).toBe('2025 FSAE Rules'); - expect(rulesets[1].name).toBe('2025 FSAE Rules2'); + expect(rulesets[0].name).toBe('2025 FSAE Rules2'); + expect(rulesets[1].name).toBe('2025 FSAE Rules'); }); }); From f7b065d0ea183cf25512752cbf6d26065646e563 Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sat, 3 Jan 2026 19:09:56 -0500 Subject: [PATCH 8/9] #3844 delete for ruleset type & ruleset --- src/backend/src/routes/rules.routes.ts | 2 - .../src/transformers/rules.transformer.ts | 2 + src/frontend/src/apis/rules.api.ts | 14 +++ src/frontend/src/hooks/rules.hooks.ts | 32 +++++- .../components/RulesetDeleteModal.tsx | 27 +++++ .../RulesPage/components/RulesetTable.tsx | 57 +++++++++- .../components/RulesetTypeDeleteModal.tsx | 27 +++++ .../RulesPage/components/RulesetTypeTable.tsx | 105 +++++++++++++----- src/frontend/src/utils/urls.ts | 4 + 9 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx create mode 100644 src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx diff --git a/src/backend/src/routes/rules.routes.ts b/src/backend/src/routes/rules.routes.ts index 124eaf24bb..3b1b4a8db1 100644 --- a/src/backend/src/routes/rules.routes.ts +++ b/src/backend/src/routes/rules.routes.ts @@ -34,8 +34,6 @@ rulesRouter.post('/rule/:ruleId/delete', RulesController.deleteRule); rulesRouter.post('/rulesetType/create', nonEmptyString(body('name')), validateInputs, RulesController.createRulesetType); -rulesRouter.post('/rule/:ruleId/delete', RulesController.deleteRule); - rulesRouter.post( '/projectRule/create', nonEmptyString(body('ruleId')), diff --git a/src/backend/src/transformers/rules.transformer.ts b/src/backend/src/transformers/rules.transformer.ts index 8b3301c4be..51959bdb48 100644 --- a/src/backend/src/transformers/rules.transformer.ts +++ b/src/backend/src/transformers/rules.transformer.ts @@ -35,6 +35,8 @@ export const rulesetTypeTransformer = (rulesetType: any): RulesetType => { name: rulesetType.name, lastUpdated: rulesetType.lastUpdated, revisionFiles: rulesetType.revisionFiles + ? rulesetType.revisionFiles.filter((ruleset: any) => ruleset.deletedByUserId === null) + : [] }; }; diff --git a/src/frontend/src/apis/rules.api.ts b/src/frontend/src/apis/rules.api.ts index 8cdd80e7cc..962facde88 100644 --- a/src/frontend/src/apis/rules.api.ts +++ b/src/frontend/src/apis/rules.api.ts @@ -74,3 +74,17 @@ export const getRulesetsByRulesetType = (rulesetTypeId: string) => { export const updateRuleset = (rulesetId: string, name: string, isActive: boolean) => { return axios.post(apiUrls.rulesetUpdate(rulesetId), { name, isActive }); }; + +/** + * Deletes a ruleset given its ID + */ +export const deleteRuleset = (rulesetId: string) => { + return axios.post(apiUrls.rulesetDelete(rulesetId)); +}; + +/** + * Deletes a ruleset type given its ID + */ +export const deleteRulesetType = (rulesetTypeId: string) => { + return axios.post(apiUrls.rulesetTypeDelete(rulesetTypeId)); +}; diff --git a/src/frontend/src/hooks/rules.hooks.ts b/src/frontend/src/hooks/rules.hooks.ts index 6825d64f5b..7ba3ef0a7a 100644 --- a/src/frontend/src/hooks/rules.hooks.ts +++ b/src/frontend/src/hooks/rules.hooks.ts @@ -13,7 +13,9 @@ import { createRulesetType, getAllRulesetTypes, getRulesetsByRulesetType, - updateRuleset + updateRuleset, + deleteRuleset, + deleteRulesetType } from '../apis/rules.api'; interface CreateRulesetTypePayload { @@ -115,3 +117,31 @@ export const useUpdateRuleset = () => { } ); }; + +export const useDeleteRuleset = () => { + const queryClient = useQueryClient(); + return useMutation( + async (rulesetId: string) => { + await deleteRuleset(rulesetId); + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesets']); + } + } + ); +}; + +export const useDeleteRulesetType = () => { + const queryClient = useQueryClient(); + return useMutation( + async (rulesetTypeId: string) => { + await deleteRulesetType(rulesetTypeId); + }, + { + onSuccess: () => { + queryClient.invalidateQueries(['rulesetTypes']); + } + } + ); +}; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx b/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx new file mode 100644 index 0000000000..911d4c8d00 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetDeleteModal.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material'; +import NERModal from '../../../components/NERModal'; + +interface RulesetDeleteModalProps { + rulesetName: string; + onDelete: () => void; + onHide: () => void; +} + +const RulesetDeleteModal: React.FC = ({ rulesetName, onDelete, onHide }) => { + return ( + + Are you sure you want to delete this ruleset? + {rulesetName} + + ); +}; + +export default RulesetDeleteModal; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index 3220167d43..cdfb88f193 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, Paper, @@ -14,22 +14,31 @@ import { CardContent, Typography, Stack, - Checkbox + Checkbox, + IconButton } from '@mui/material'; import { datePipe } from '../../../utils/pipes'; import { NERButton } from '../../../components/NERButton'; import { useHistory, useParams } from 'react-router-dom'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; -import { useRulesetsByType, useUpdateRuleset } from '../../../hooks/rules.hooks'; +import { useDeleteRuleset, useRulesetsByType, useUpdateRuleset } from '../../../hooks/rules.hooks'; import { Ruleset } from 'shared'; import { routes } from '../../../utils/routes'; import { useToast } from '../../../hooks/toasts.hooks'; +import { Delete } from '@mui/icons-material'; +import RulesetDeleteModal from './RulesetDeleteModal'; interface RulesetParams { rulesetTypeId: string; } +interface RulesetDeleteButtonProps { + rulesetId: string; + name: string; + onDelete: (rulesetId: string, name: string) => void; +} + const RulesetTable: React.FC = () => { const { rulesetTypeId } = useParams(); const toast = useToast(); @@ -42,6 +51,7 @@ const RulesetTable: React.FC = () => { const { data: rulesets = [], isLoading, error } = useRulesetsByType(rulesetTypeId); const updateRuleset = useUpdateRuleset(); + const { mutateAsync: deleteRuleset } = useDeleteRuleset(); // Table header configuration const headCells = [ @@ -50,7 +60,8 @@ const RulesetTable: React.FC = () => { { id: 'percentRulesAssigned', label: '% of Rules Assigned' }, { id: 'car', label: 'Car' }, { id: 'isActive', label: 'Active?' }, - { id: 'actions', label: 'Actions' } + { id: 'actions', label: 'Actions' }, + { id: 'delete', label: '' } ]; const handleToggleActive = (ruleset: Ruleset) => { @@ -80,6 +91,36 @@ const RulesetTable: React.FC = () => { history.push(routes.RULESET_VIEW.replace(':rulesetId', rulesetId)); }; + const handleDeleteRuleset = async (rulesetId: string, name: string) => { + try { + await deleteRuleset(rulesetId); + toast.success(`Ruleset: ${name} deleted successfully!`); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const RulesetDeleteButton: React.FC = ({ rulesetId, name, onDelete }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const handleDeleteSubmit = () => { + onDelete(rulesetId, name); + setShowDeleteModal(false); + }; + return ( + <> + setShowDeleteModal(true)}> + + + {showDeleteModal && ( + setShowDeleteModal(false)} /> + )} + + ); + }; + if (isLoading) return ; if (error) return ; @@ -174,6 +215,7 @@ const RulesetTable: React.FC = () => { > View Rules + @@ -263,6 +305,13 @@ const RulesetTable: React.FC = () => { View Rules + + + )) )} diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx new file mode 100644 index 0000000000..ad48f500d2 --- /dev/null +++ b/src/frontend/src/pages/RulesPage/components/RulesetTypeDeleteModal.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material'; +import NERModal from '../../../components/NERModal'; + +interface RulesetTypeDeleteModalProps { + rulesetTypeName: string; + onDelete: () => void; + onHide: () => void; +} + +const RulesetTypeDeleteModal: React.FC = ({ rulesetTypeName, onDelete, onHide }) => { + return ( + + Are you sure you want to delete this ruleset type? + {rulesetTypeName} + + ); +}; + +export default RulesetTypeDeleteModal; diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx index 6350dc4bec..4ef2c1c2eb 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTypeTable.tsx @@ -13,30 +13,42 @@ import { CardContent, Typography, Stack, - Link + IconButton } from '@mui/material'; -import { Link as RouterLink, useHistory } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { datePipe } from '../../../utils/pipes'; import { routes } from '../../../utils/routes'; -import { useAllRulesetTypes } from '../../../hooks/rules.hooks'; +import { useAllRulesetTypes, useDeleteRulesetType } from '../../../hooks/rules.hooks'; import LoadingIndicator from '../../../components/LoadingIndicator'; import ErrorPage from '../../ErrorPage'; import { RulesetType } from 'shared'; import { NERButton } from '../../../components/NERButton'; +import { useToast } from '../../../hooks/toasts.hooks'; +import { useState } from 'react'; +import RulesetTypeDeleteModal from './RulesetTypeDeleteModal'; +import { Delete } from '@mui/icons-material'; -type RulesetTypeColumnId = 'id' | 'name' | 'lastUpdated' | 'revisions' | 'actions'; +type RulesetTypeColumnId = 'id' | 'name' | 'lastUpdated' | 'revisions' | 'actions' | 'delete'; interface RulesetTypeHeadCell { id: RulesetTypeColumnId; label: string; } +interface RulesetTypeDeleteButtonProps { + rulesetTypeId: string; + name: string; + onDelete: (rulesetTypeId: string, name: string) => void; +} + const RulesetTypeTable: React.FC = () => { const history = useHistory(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const toast = useToast(); const { data: rulesetTypes = [], isLoading, error } = useAllRulesetTypes(); + const { mutateAsync: deleteRulesetType } = useDeleteRulesetType(); const headCells: readonly RulesetTypeHeadCell[] = [ { @@ -54,6 +66,10 @@ const RulesetTypeTable: React.FC = () => { { id: 'actions', label: 'Actions' + }, + { + id: 'delete', + label: '' } ]; @@ -61,6 +77,47 @@ const RulesetTypeTable: React.FC = () => { history.push(routes.RULESET_BY_ID.replace(':rulesetTypeId', rulesetTypeId)); }; + const handleDeleteRulesetType = async (rulesetTypeId: string, name: string) => { + const rulesetType = rulesetTypes.find((rt) => rt.rulesetTypeId === rulesetTypeId); + if (rulesetType && rulesetType.revisionFiles.length > 0) { + toast.error('Cannot delete ruleset type with existing revisions'); + return; + } + + try { + await deleteRulesetType(rulesetTypeId); + toast.success(`Ruleset Type: ${name} deleted successfully!`); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + const RulesetTypeDeleteButton: React.FC = ({ rulesetTypeId, name, onDelete }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const handleDeleteSubmit = () => { + onDelete(rulesetTypeId, name); + setShowDeleteModal(false); + }; + + return ( + <> + setShowDeleteModal(true)}> + + + {showDeleteModal && ( + setShowDeleteModal(false)} + /> + )} + + ); + }; + if (isLoading) return ; if (error) return ; @@ -115,6 +172,11 @@ const RulesetTypeTable: React.FC = () => { > View Rulesets + @@ -154,32 +216,10 @@ const RulesetTypeTable: React.FC = () => { }} > - - {rulesetType.name} - - - - - {datePipe(rulesetType.lastUpdated)} - - - - - {rulesetType.revisionFiles.length} - + {rulesetType.name} + {datePipe(rulesetType.lastUpdated)} + {rulesetType.revisionFiles.length} { View Rulesets + + + )) )} diff --git a/src/frontend/src/utils/urls.ts b/src/frontend/src/utils/urls.ts index 68f7353af8..3a3373c227 100644 --- a/src/frontend/src/utils/urls.ts +++ b/src/frontend/src/utils/urls.ts @@ -448,6 +448,8 @@ const ruleset = () => `${rules()}/ruleset`; const rulesetTypeCreate = () => `${rules()}/rulesetType/create`; const rulesetsCreate = () => `${ruleset()}/create`; const rulesetUpdate = (rulesetId: string) => `${ruleset()}/${rulesetId}/update`; +const rulesetDelete = (rulesetId: string) => `${ruleset()}/${rulesetId}/delete`; +const rulesetTypeDelete = (rulesetTypeId: string) => `${rules()}/rulesetType/${rulesetTypeId}/delete`; /**************** Other Endpoints ****************/ const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`; @@ -762,6 +764,8 @@ export const apiUrls = { rulesetTypeCreate, rulesetsCreate, rulesetUpdate, + rulesetDelete, + rulesetTypeDelete, version }; From e98cec5465d3eeef767bc150a9d6f6a2d219c17f Mon Sep 17 00:00:00 2001 From: Ciel Bellerose Date: Sun, 4 Jan 2026 12:29:51 -0500 Subject: [PATCH 9/9] #3844 added cannot delete active ruleset logic --- src/backend/src/services/rules.services.ts | 8 +++- src/backend/tests/unit/rule.test.ts | 44 +++++++++++++++++++ .../RulesPage/components/RulesetTable.tsx | 9 +++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/backend/src/services/rules.services.ts b/src/backend/src/services/rules.services.ts index 2bbabb2e04..4d9df5342b 100644 --- a/src/backend/src/services/rules.services.ts +++ b/src/backend/src/services/rules.services.ts @@ -486,9 +486,13 @@ export default class RulesService { if (!hasPermission) throw new AccessDeniedException('Only admins can delete a ruleset.'); + if (ruleset.active) { + throw new HttpException(400, 'Cannot delete an active ruleset. Please deactivate it first.'); + } + const deletedRuleset = await prisma.ruleset.update({ where: { rulesetId }, - data: { deletedBy: { connect: { userId: deleterId } } }, + data: { deletedBy: { connect: { userId: deleterId } }, active: false }, ...getRulesetQueryArgs() }); @@ -943,7 +947,7 @@ export default class RulesService { rulesetTypeId: rulesetExists.rulesetTypeId, organizationId }, - dateDeleted: null + deletedByUserId: null } }); diff --git a/src/backend/tests/unit/rule.test.ts b/src/backend/tests/unit/rule.test.ts index 93247bb4d2..b8dbced304 100644 --- a/src/backend/tests/unit/rule.test.ts +++ b/src/backend/tests/unit/rule.test.ts @@ -377,6 +377,12 @@ describe('Create Rules Tests', () => { }); it('Successful get rulesets by ruleset types after deleting ruleset', async () => { + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId }, + data: { active: false } + }); + await RulesService.deleteRuleset(rulesetId, batman.userId, orgId); const rulesets = await RulesService.getRulesetsByRulesetType(rulesetType.rulesetTypeId, orgId); expect(rulesets.length).toBe(0); @@ -524,6 +530,11 @@ describe('Create Rules Tests', () => { expect(ruleset2.name).toBe('name2'); }); it('update ruleset status on deleted ruleset fails', async () => { + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId }, + data: { active: false } + }); await RulesService.deleteRuleset(rulesetId, batman.userId, orgId); await expect(async () => await RulesService.updateRuleset(batman, orgId, rulesetId, 'name', false)).rejects.toThrow( new NotFoundException('Ruleset', rulesetId) @@ -973,6 +984,13 @@ describe('Rule Tests', () => { it('Deletes a ruleset successfully and returns the correct information', async () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); + + // deactivate before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + const totalRules = await prisma.rule.count({ where: { rulesetId: ruleset1.rulesetId } }); @@ -989,6 +1007,20 @@ describe('Rule Tests', () => { expect(deleted.rulesetId).toBe(ruleset1.rulesetId); expect(deleted.assignedPercentage).toBeCloseTo(expectedPercentage, 2); }); + it('Throws error when trying to delete an active ruleset', async () => { + const car = await createUniqueCar(orgId); + const { ruleset1 } = await setupRules(car); + + // Ensure the ruleset is active + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: true } + }); + + await expect( + RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId) + ).rejects.toThrow('Cannot delete an active ruleset. Please deactivate it first.'); + }); it('Delete ruleset fails if user does not have permission', async () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); @@ -1001,6 +1033,12 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); await expect( async () => await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId) @@ -1455,6 +1493,12 @@ describe('Rule Tests', () => { const car = await createUniqueCar(orgId); const { ruleset1 } = await setupRules(car); + // Deactivate the ruleset before deleting + await prisma.ruleset.update({ + where: { rulesetId: ruleset1.rulesetId }, + data: { active: false } + }); + await RulesService.deleteRuleset(ruleset1.rulesetId, admin.userId, organization.organizationId); await expect(RulesService.getUnassignedRules(ruleset1.rulesetId, organization)).rejects.toThrow( diff --git a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx index cdfb88f193..63ede00b2c 100644 --- a/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx +++ b/src/frontend/src/pages/RulesPage/components/RulesetTable.tsx @@ -92,6 +92,13 @@ const RulesetTable: React.FC = () => { }; const handleDeleteRuleset = async (rulesetId: string, name: string) => { + const ruleset = rulesets.find((r) => r.rulesetId === rulesetId); + + if (ruleset && ruleset.active) { + toast.error('Cannot delete an active ruleset. Please deactivate it first.'); + return; + } + try { await deleteRuleset(rulesetId); toast.success(`Ruleset: ${name} deleted successfully!`); @@ -243,7 +250,7 @@ const RulesetTable: React.FC = () => { {/* Table rows with ruleset data */} {rulesets.length === 0 ? ( - + No Rulesets Found