-
Notifications
You must be signed in to change notification settings - Fork 9
#4286 add rule status to general view #4307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wavehassman
merged 3 commits into
feature/rules-dashboard
from
#4286-rules-toggle-status-in-general-view
Jul 6, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,21 +34,15 @@ import { | |
| useCreateProjectRule | ||
| } from '../../../../hooks/rules.hooks'; | ||
| import { useToast } from '../../../../hooks/toasts.hooks'; | ||
| import { InfoOutlined, KeyboardArrowRight, KeyboardArrowDown } from '@mui/icons-material'; | ||
| import { InfoOutlined } from '@mui/icons-material'; | ||
| import { useHistory } from 'react-router-dom'; | ||
| import { routes } from '../../../../utils/routes'; | ||
| import RuleStatusTag from '../../../RulesPage/components/RuleStatusTag'; | ||
|
|
||
| interface ProjectRulesTabProps { | ||
| project: Project; | ||
| } | ||
|
|
||
| /** | ||
| * Get the status chip configuration | ||
| */ | ||
| const getStatusConfig = (isComplete: boolean) => { | ||
| return isComplete ? { label: 'Complete', color: '#4caf50' } : { label: 'Incomplete', color: '#f44336' }; | ||
| }; | ||
|
|
||
| export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { | ||
| const toast = useToast(); | ||
| const theme = useTheme(); | ||
|
|
@@ -100,27 +94,6 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { | |
| return allRules.filter((rule) => !rule.parentRule); | ||
| }, [allRules]); | ||
|
|
||
| // Helper function to get all descendant leaf rules for a given rule | ||
| const getDescendantLeafRules = (rule: Rule): Rule[] => { | ||
| const children = allRules.filter((r) => r.parentRule?.ruleId === rule.ruleId); | ||
| if (children.length === 0) { | ||
| // This is a leaf rule | ||
| return [rule]; | ||
| } | ||
| // Recursively get leaf rules from all children | ||
| return children.flatMap((child) => getDescendantLeafRules(child)); | ||
| }; | ||
|
|
||
| // Helper function to calculate aggregated completion from leaf rules. | ||
| // A parent is complete only if all of its descendant leaf rules are complete. | ||
| const getAggregatedStatus = (rule: Rule): boolean => { | ||
| const leafRules = getDescendantLeafRules(rule); | ||
| if (leafRules.length === 0) { | ||
| return false; | ||
| } | ||
| return leafRules.every((leafRule) => leafRule.isComplete); | ||
| }; | ||
|
|
||
| // Handle completion update | ||
| const handleStatusUpdate = async (ruleId: string, isComplete: boolean) => { | ||
| try { | ||
|
|
@@ -193,77 +166,19 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { | |
| // Check if we have no active ruleset | ||
| const hasNoActiveRuleset = !activeRulesetLoading && !activeRuleset; | ||
|
|
||
| // Right content for rule rows - status badge | ||
| // Right content for rule rows - status badge. Leaf rules are clickable to open | ||
| // the completion popover; parents show an aggregated, read-only status. | ||
| const renderRightContent = (rule: Rule) => { | ||
| const hasChildren = allRules.some((r) => r.parentRule?.ruleId === rule.ruleId); | ||
| const isLeafRule = !hasChildren; | ||
|
|
||
| // Completion - for leaf rules use their own, for parents aggregate from children | ||
| const isComplete = isLeafRule ? rule.isComplete : getAggregatedStatus(rule); | ||
| const statusConfig = getStatusConfig(isComplete); | ||
|
|
||
| const completedByName = rule.completedBy && `${rule.completedBy.firstName} ${rule.completedBy.lastName}`; | ||
| const completionMessage = completedByName | ||
| ? `Completed by ${completedByName}${rule.completedInProject ? ` in ${rule.completedInProject.projectName}` : ''}` | ||
| : ''; | ||
|
|
||
| // Whether the status popover is currently open for this rule | ||
| const isLeafRule = !allRules.some((r) => r.parentRule?.ruleId === rule.ruleId); | ||
| const isPopoverOpenForRule = Boolean(statusPopoverAnchor) && selectedProjectRule?.rule.ruleId === rule.ruleId; | ||
|
|
||
| return ( | ||
| <Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved into RuleStatusTag |
||
| {isLeafRule && isComplete && completionMessage && ( | ||
| <Tooltip title={completionMessage} arrow> | ||
| <IconButton | ||
| size="small" | ||
| onClick={(e) => e.stopPropagation()} | ||
| sx={{ | ||
| padding: '2px', | ||
| color: 'text.secondary' | ||
| }} | ||
| > | ||
| <InfoOutlined fontSize="small" /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| )} | ||
| <Box | ||
| onClick={ | ||
| isLeafRule | ||
| ? (e: React.MouseEvent<HTMLElement>) => { | ||
| e.stopPropagation(); | ||
| handleStatusClick(e, rule); | ||
| } | ||
| : undefined | ||
| } | ||
| sx={{ | ||
| backgroundColor: statusConfig.color, | ||
| color: 'white', | ||
| fontSize: '11px', | ||
| fontWeight: 600, | ||
| pl: isLeafRule ? 0.25 : 0.75, | ||
| pr: 0.75, | ||
| py: 0.25, | ||
| borderRadius: '3px', | ||
| cursor: isLeafRule ? 'pointer' : 'default', | ||
| display: 'inline-flex', | ||
| alignItems: 'center', | ||
| whiteSpace: 'nowrap', | ||
| '&:hover': isLeafRule | ||
| ? { | ||
| opacity: 0.85 | ||
| } | ||
| : {} | ||
| }} | ||
| > | ||
| {isLeafRule && | ||
| (isPopoverOpenForRule ? ( | ||
| <KeyboardArrowDown sx={{ fontSize: '16px', mr: 0.25 }} /> | ||
| ) : ( | ||
| <KeyboardArrowRight sx={{ fontSize: '16px', mr: 0.25 }} /> | ||
| ))} | ||
| {statusConfig.label} | ||
| </Box> | ||
| </Box> | ||
| <RuleStatusTag | ||
| rule={rule} | ||
| allRules={allRules} | ||
| popoverOpen={isPopoverOpenForRule} | ||
| onClick={isLeafRule ? (e) => handleStatusClick(e, rule) : undefined} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
|
|
@@ -432,7 +347,7 @@ export const ProjectRulesTab = ({ project }: ProjectRulesTabProps) => { | |
| <UpdateStatusPopover | ||
| anchorEl={statusPopoverAnchor} | ||
| onClose={handleStatusPopoverClose} | ||
| projectRule={selectedProjectRule} | ||
| rule={selectedProjectRule.rule} | ||
| onStatusChange={handleStatusUpdate} | ||
| /> | ||
| )} | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
src/frontend/src/pages/RulesPage/components/RuleStatusTag.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| * 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, IconButton, Tooltip } from '@mui/material'; | ||
| import { InfoOutlined, KeyboardArrowRight, KeyboardArrowDown } from '@mui/icons-material'; | ||
| import { Rule } from 'shared'; | ||
| import { getRuleStatusConfig, isRuleComplete } from '../../../utils/rules.utils'; | ||
|
|
||
| interface RuleStatusTagProps { | ||
| rule: Rule; | ||
| allRules: Rule[]; | ||
| // ability to update completion status | ||
| onClick?: (event: React.MouseEvent<HTMLElement>) => void; | ||
| // controls chevron direction when completion is interactive | ||
| popoverOpen?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Completion status chip for a rule. | ||
| * A leaf shows its own completion, while a parent is only complete if all | ||
| * of its descendant leaf rules are complete. Completed leafs also show an | ||
| * info tooltip with who completed it and in which project. | ||
| */ | ||
| const RuleStatusTag: React.FC<RuleStatusTagProps> = ({ rule, allRules, onClick, popoverOpen = false }) => { | ||
| const isComplete = isRuleComplete(rule, allRules); | ||
| const { label, color } = getRuleStatusConfig(isComplete); | ||
|
|
||
| const isLeaf = !allRules.some((r) => r.parentRule?.ruleId === rule.ruleId); | ||
| // Note: Info tooltip only says "Completed by {User}" if completed in general view | ||
| const completedByName = rule.completedBy && `${rule.completedBy.firstName} ${rule.completedBy.lastName}`; | ||
| const completionMessage = completedByName | ||
| ? `Completed by ${completedByName}${rule.completedInProject ? ` in ${rule.completedInProject.projectName}` : ''}` | ||
| : ''; | ||
|
|
||
| // only leafs are interactive | ||
| const isInteractive = isLeaf && Boolean(onClick); | ||
|
|
||
| return ( | ||
| <Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}> | ||
| {isLeaf && isComplete && completionMessage && ( | ||
| <Tooltip title={completionMessage} arrow> | ||
| <IconButton size="small" onClick={(e) => e.stopPropagation()} sx={{ padding: '2px', color: 'text.secondary' }}> | ||
| <InfoOutlined fontSize="small" /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| )} | ||
| <Box | ||
| onClick={ | ||
| isInteractive | ||
| ? (e: React.MouseEvent<HTMLElement>) => { | ||
| e.stopPropagation(); | ||
| onClick!(e); | ||
| } | ||
| : undefined | ||
| } | ||
| sx={{ | ||
| backgroundColor: color, | ||
| color: 'white', | ||
| fontSize: '11px', | ||
| fontWeight: 600, | ||
| pl: isInteractive ? 0.25 : 0.75, | ||
| pr: 0.75, | ||
| py: 0.25, | ||
| borderRadius: '3px', | ||
| cursor: isInteractive ? 'pointer' : 'default', | ||
| display: 'inline-flex', | ||
| alignItems: 'center', | ||
| whiteSpace: 'nowrap', | ||
| '&:hover': isInteractive ? { opacity: 0.85 } : {} | ||
| }} | ||
| > | ||
| {isInteractive && | ||
| (popoverOpen ? ( | ||
| <KeyboardArrowDown sx={{ fontSize: '16px', mr: 0.25 }} /> | ||
| ) : ( | ||
| <KeyboardArrowRight sx={{ fontSize: '16px', mr: 0.25 }} /> | ||
| ))} | ||
| {label} | ||
| </Box> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default RuleStatusTag; |
76 changes: 65 additions & 11 deletions
76
src/frontend/src/pages/RulesPage/components/RulesetGeneralView.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved into util functions