Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/frontend/src/apis/rules.api.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
// write api functions below here!
import axios from '../utils/axios';

export const deleteRule = (ruleId: string) => {
return axios.post(`/rules/rule/${ruleId}/delete`);
};
25 changes: 24 additions & 1 deletion src/frontend/src/hooks/rules.hooks.ts
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
// write hooks below here!
import { useMutation, useQueryClient } from 'react-query';
import { deleteRule } from '../apis/rules.api';
import { useToast } from './toasts.hooks';

export const useDeleteRule = () => {
const queryClient = useQueryClient();
const toast = useToast();

return useMutation<void, Error, string>(
['rules', 'delete'],
async (ruleId: string) => {
await deleteRule(ruleId);
},
{
onSuccess: () => {
toast.success('Rule deleted successfully');
queryClient.invalidateQueries(['rulesets']);
},
onError: (error: Error) => {
toast.error(error.message);
}
}
);
};
116 changes: 116 additions & 0 deletions src/frontend/src/pages/RulesPage/RulesComponents/DeleteRuleModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 { Box, Typography, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { Rule } from 'shared';
import WarningIcon from '@mui/icons-material/Warning';

interface DeleteRuleModalProps {
open: boolean;
onHide: () => void;
onConfirm: () => void;
rule: Rule;
totalRulesToDelete: number;
}

const DeleteRuleModal = ({ open, onHide, onConfirm, rule, totalRulesToDelete }: DeleteRuleModalProps) => {
const hasChildren = rule.subRuleIds.length > 0;
const titlePrefix = hasChildren ? 'Delete Rule Section:' : 'Delete Rule:';

const modalTitle = rule.ruleContent
? `${titlePrefix} ${rule.ruleCode} - ${rule.ruleContent}`
: `${titlePrefix} ${rule.ruleCode}`;

return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good but we should use the prebuilt NERModal so it better matches the rest of the website. A good example might be the AdminToolsPage/ProjectsConfig/PartTagDeleteModal

<Dialog
open={open}
onClose={onHide}
PaperProps={{
sx: {
borderRadius: '20px',
width: '700px',
maxWidth: '700px',
backgroundColor: '#3a3a3a'
}
}}
>
{/* Header */}
<DialogTitle
sx={{
backgroundColor: '#ef4345',
minHeight: '90px',
display: 'flex',
alignItems: 'center',
color: '#ffffff',
fontSize: '2rem',
fontWeight: 700,
paddingLeft: '24px'
}}
>
Confirm Deletion
</DialogTitle>

{/* Body */}
<DialogContent
sx={{
backgroundColor: '#3a3a3a',
'&.MuiDialogContent-root': { paddingTop: '24px' }
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography sx={{ color: '#ffffff', fontWeight: 400, fontSize: '1.25rem' }}>{modalTitle}</Typography>

<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<WarningIcon sx={{ color: '#ef4345', fontSize: 30 }} />
<Typography sx={{ color: '#ffffff', fontWeight: 600, fontSize: '1.25rem' }}>
{totalRulesToDelete} {totalRulesToDelete === 1 ? 'rule' : 'rules'} will be deleted
</Typography>
</Box>
</Box>
</DialogContent>

{/* Footer */}
<DialogActions sx={{ backgroundColor: '#3a3a3a', pb: 2, pr: 2 }}>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
onClick={onHide}
sx={{
backgroundColor: '#ef4345',
color: '#ffffff',
fontSize: '1.125rem',
paddingY: 0,
px: 1.5,
minHeight: 0,
textTransform: 'none',
borderRadius: '8px',
'&:hover': { backgroundColor: '#d63c37' }
}}
>
Cancel
</Button>

<Button
onClick={onConfirm}
sx={{
backgroundColor: '#ef4345',
color: '#ffffff',
fontSize: '1.125rem',
paddingY: 0,
px: 1.5,
minHeight: 0,
textTransform: 'none',
borderRadius: '8px',
'&:hover': { backgroundColor: '#d63c37' }
}}
>
Delete
</Button>
</Box>
</DialogActions>
</Dialog>
);
};

export default DeleteRuleModal;
42 changes: 40 additions & 2 deletions src/frontend/src/pages/RulesPage/RulesetPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import RuleActions from './RuleActions';
import { Rule } from 'shared';
import ErrorPage from '../ErrorPage';
import LoadingIndicator from '../../components/LoadingIndicator';
import DeleteRuleModal from './RulesComponents/DeleteRuleModal';
import { useDeleteRule } from '../../hooks/rules.hooks';
import { countRulesToDelete } from '../../utils/rules.utils';

/**
* Placeholder hook to fetch a single ruleset.
Expand Down Expand Up @@ -158,9 +161,12 @@ const useSingleRuleset = (rulesetId: string) => {
const RulesetPage: React.FC = () => {
const { rulesetId } = useParams<{ rulesetId: string; tabValue?: string }>();
const [tabValue, setTabValue] = useState(0);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [ruleToDelete, setRuleToDelete] = useState<Rule | null>(null);
const defaultTab = 'edit-rules';

const { data: ruleset, isError, error, isLoading } = useSingleRuleset(rulesetId);
const { mutateAsync: deleteRuleMutation } = useDeleteRule();

const tabs = [
{ tabUrlValue: 'edit-rules', tabName: 'Edit Rules' },
Expand All @@ -185,15 +191,37 @@ const RulesetPage: React.FC = () => {
};

const handleRemoveRule = (ruleId: string) => {
// Placeholder
console.log('Remove rule:', ruleId);
const rule = ruleset.rules.find((r) => r.ruleId === ruleId);
if (rule) {
setRuleToDelete(rule);
setDeleteModalOpen(true);
}
};

const handleEditRule = (ruleId: string) => {
// Placeholder
console.log('Edit rule:', ruleId);
};

const handleDeleteConfirm = async () => {
if (!ruleToDelete) return;

try {
await deleteRuleMutation(ruleToDelete.ruleId);
setDeleteModalOpen(false);
setRuleToDelete(null);
} catch (error) {
console.error('Failed to delete rule:', error);
}
};

const handleDeleteCancel = () => {
setDeleteModalOpen(false);
setRuleToDelete(null);
};

const totalRulesToDelete = ruleToDelete ? countRulesToDelete(ruleToDelete, ruleset.rules) : 0;

// Filter to only show top-level rules
const topLevelRules = ruleset.rules.filter((rule) => !rule.parentRule);

Expand Down Expand Up @@ -286,6 +314,16 @@ const RulesetPage: React.FC = () => {
<Box>{/* Assign Rules tab content will be added in a future ticket */}</Box>
)}
</Box>

{ruleToDelete && (
<DeleteRuleModal
open={deleteModalOpen}
onHide={handleDeleteCancel}
onConfirm={handleDeleteConfirm}
rule={ruleToDelete}
totalRulesToDelete={totalRulesToDelete}
/>
)}
</PageLayout>
);
};
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/src/utils/rules.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Rule } from 'shared';

export const countRulesToDelete = (rule: Rule, allRules: Rule[]): number => {
let count = 1;
const children = allRules.filter((r) => rule.subRuleIds.includes(r.ruleId));
for (const child of children) {
count += countRulesToDelete(child, allRules);
}
return count;
};