Skip to content
Merged
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
53 changes: 33 additions & 20 deletions src/frontend/src/pages/RulesPage/RuleActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import EditIcon from '@mui/icons-material/Edit';

interface RuleActionsProps {
ruleId: string;
onAdd: (ruleId: string) => void;
onAdd: (ruleId: string, anchorEl: HTMLElement) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

a prop shouldn't take in an anchorEl because it tightly couples this component with its parent. Instead, we need to figure out what information the onAdd needs, and extract that from the element before passing it through (if we need it at all)

onRemove: (ruleId: string) => void;
onEdit: (ruleId: string) => void;
iconColor?: string;
Expand All @@ -21,27 +21,40 @@ interface RuleActionsProps {
* Supports adding, removing, and editing a rule.
*/
const RuleActions: React.FC<RuleActionsProps> = ({ ruleId, onAdd, onRemove, onEdit, iconColor = '#000000' }) => {
const actions = [
{ icon: <AddCircleOutlineIcon fontSize="small" />, handler: onAdd },
{ icon: <RemoveCircleOutlineIcon fontSize="small" />, handler: onRemove },
{ icon: <EditIcon fontSize="small" />, handler: onEdit }
];

return (
<Box sx={{ display: 'flex', gap: 0.25, alignItems: 'center', justifyContent: 'center' }}>
{actions.map((action, index) => (
<IconButton
key={index}
size="small"
onClick={(e) => {
e.stopPropagation();
action.handler(ruleId);
}}
sx={{ padding: 0.25, color: iconColor }}
>
{action.icon}
</IconButton>
))}
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onAdd(ruleId, e.currentTarget);
}}
sx={{ padding: 0.25, color: iconColor }}
>
<AddCircleOutlineIcon fontSize="small" />
</IconButton>

<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onRemove(ruleId);
}}
sx={{ padding: 0.25, color: iconColor }}
>
<RemoveCircleOutlineIcon fontSize="small" />
</IconButton>

<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onEdit(ruleId);
}}
sx={{ padding: 0.25, color: iconColor }}
>
<EditIcon fontSize="small" />
</IconButton>
</Box>
);
};
Expand Down
53 changes: 49 additions & 4 deletions src/frontend/src/pages/RulesPage/RulesetEditPage.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 AddRuleSectionModal from './components/AddRuleSectionModal';
import AddRuleModal from './components/AddRuleModal';
import { AddRuleBox } from './components/AddRuleBox';
import AssignRulesTab from './AssignRulesTab';

/**
Expand Down Expand Up @@ -161,6 +164,14 @@ const RulesetEditPage: React.FC = () => {
const [tabValue, setTabValue] = useState(0);
const defaultTab = 'edit-rules';

const [showAddMenu, setShowAddMenu] = useState(false);
const [addMenuAnchorEl, setAddMenuAnchorEl] = useState<HTMLElement | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

if I'm understanding this correctly this useState is to keep track of which element the addMenu is on, in which case keeping track of the html element is too tightly coupling this component with its children. Instead I think it might make more sense to just have a single optional useState for which rule to show the addMenu on

const [activeRuleId, setActiveRuleId] = useState<string | null>(null);

// temporary placeholder useState fns for the add rule section and add rule modals
const [showAddRuleSectionModal, setShowAddRuleSectionModal] = useState(false);
const [showAddRuleModal, setShowAddRuleModal] = useState(false);

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

const tabs = [
Expand All @@ -180,9 +191,32 @@ const RulesetEditPage: React.FC = () => {
// Placeholder
};

const handleAddRule = (ruleId: string) => {
// Placeholder
console.log('Add rule to:', ruleId);
const handleOpenAddMenu = (ruleId: string, anchorEl: HTMLElement) => {
// trying to make tests run lol this comment can get deleted later
if (showAddMenu && addMenuAnchorEl === anchorEl) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be handled in the child component because it should know whether or not it has a menu open. This will also make it so we don't have to pass the anchor element through at all

handleCloseAddMenu();
return;
}

setActiveRuleId(ruleId);
setAddMenuAnchorEl(anchorEl);
setShowAddMenu(true);
};

const handleCloseAddMenu = () => {
setShowAddMenu(false);
setAddMenuAnchorEl(null);
};

const handleAddRuleSectionFromMenu = () => {
setShowAddRuleSectionModal(true);
handleCloseAddMenu();
};

const handleAddRuleFromMenu = () => {
console.log('Add rule to:', activeRuleId);
setShowAddRuleModal(true);
handleCloseAddMenu();
};

const handleRemoveRule = (ruleId: string) => {
Expand Down Expand Up @@ -227,7 +261,7 @@ const RulesetEditPage: React.FC = () => {
rightContent={(currentRule) => (
<RuleActions
ruleId={currentRule.ruleId}
onAdd={handleAddRule}
onAdd={handleOpenAddMenu}
onRemove={handleRemoveRule}
onEdit={handleEditRule}
iconColor="#000000"
Expand All @@ -244,6 +278,17 @@ const RulesetEditPage: React.FC = () => {
</Table>
</TableContainer>

<AddRuleBox
open={showAddMenu}
anchorEl={addMenuAnchorEl}
onClose={handleCloseAddMenu}
onAddRuleSection={handleAddRuleSectionFromMenu}
onAddRule={handleAddRuleFromMenu}
/>

<AddRuleSectionModal open={showAddRuleSectionModal} onClose={() => setShowAddRuleSectionModal(false)} />
<AddRuleModal open={showAddRuleModal} onClose={() => setShowAddRuleModal(false)} />

<Box
sx={{
backgroundColor: '#121313',
Expand Down
75 changes: 75 additions & 0 deletions src/frontend/src/pages/RulesPage/components/AddRuleBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as React from 'react';
import { Box, Popover, useTheme } from '@mui/material';
import { NERButton } from '../../../components/NERButton';

type AddRuleBoxProps = {
open: boolean;
anchorEl: HTMLElement | null;
onClose: () => void;
onAddRuleSection: () => void;
onAddRule: () => void;
};

export const AddRuleBox: React.FC<AddRuleBoxProps> = ({ open, anchorEl, onClose, onAddRuleSection, onAddRule }) => {
const theme = useTheme();

return (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
disableRestoreFocus
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{
sx: {
backgroundColor: 'transparent',
boxShadow: 'none'
}
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.grey[700],
borderRadius: '8px',
overflow: 'hidden',
border: '1px solid rgba(0,0,0,0.4)',
minWidth: 'auto'
}}
>
<NERButton
onClick={onAddRuleSection}
sx={{
borderRadius: 0,
backgroundColor: 'transparent',
color: theme.palette.common.white,
lineHeight: 1.1,
justifyContent: 'flex-end',
'&:hover': { backgroundColor: 'rgba(255,255,255,0.12)' }
}}
>
Add Rule Section
</NERButton>

<Box sx={{ height: 1, backgroundColor: 'rgba(255,255,255,0.18)' }} />

<NERButton
onClick={onAddRule}
sx={{
borderRadius: 0,
borderTop: '1px solid black',
backgroundColor: 'transparent',
color: theme.palette.common.white,
lineHeight: 1.1,
justifyContent: 'flex-end',
'&:hover': { backgroundColor: 'rgba(255,255,255,0.12)' }
}}
>
Add Rule
</NERButton>
</Box>
</Popover>
);
};
42 changes: 42 additions & 0 deletions src/frontend/src/pages/RulesPage/components/AddRuleModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, Typography } from '@mui/material';
import { useToast } from '../../../hooks/toasts.hooks';
import NERFormModal from '../../../components/NERFormModal';
import { useForm } from 'react-hook-form';

interface AddRuleModalProps {
open: boolean;
onClose: () => void;
}

const AddRuleModal: React.FC<AddRuleModalProps> = ({ open, onClose }) => {
const toast = useToast();

const { handleSubmit, reset } = useForm({ defaultValues: {} });

const onSubmit = async () => {
toast.success('Add Rule submitted (placeholder)');
onClose();
};

return (
<NERFormModal
open={open}
onHide={onClose}
reset={() => reset({})}
title="Add Rule"
handleUseFormSubmit={handleSubmit}
onFormSubmit={onSubmit}
formId="add-rule-form"
showCloseButton
>
<Box sx={{ py: 2 }}>
<Typography variant="body2" color="text.secondary">
This is a temporary placeholder modal for creating a new rule. The full rule creation form will be implemented
later.
</Typography>
</Box>
</NERFormModal>
);
};

export default AddRuleModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, Typography } from '@mui/material';
import NERFormModal from '../../../components/NERFormModal';
import { useToast } from '../../../hooks/toasts.hooks';
import { useForm } from 'react-hook-form';

interface AddRuleSectionModalProps {
open: boolean;
onClose: () => void;
}

const AddRuleSectionModal: React.FC<AddRuleSectionModalProps> = ({ open, onClose }) => {
const toast = useToast();

const { handleSubmit, reset } = useForm({ defaultValues: {} });

const onSubmit = async () => {
toast.success('Add Rule Section submitted (placeholder)');
onClose();
};

return (
<NERFormModal
open={open}
onHide={onClose}
reset={() => reset({})}
title="Add Rule Section"
handleUseFormSubmit={handleSubmit}
onFormSubmit={onSubmit}
formId="add-rule-section-form"
showCloseButton
>
<Box sx={{ py: 2 }}>
<Typography variant="body2" color="text.secondary">
This is a temporary placeholder modal for creating a new rule section. Form fields will be added in a future
ticket.
</Typography>
</Box>
</NERFormModal>
);
};

export default AddRuleSectionModal;
Loading