github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/public/src/components/Groups.tsx (about) 1 import React from "react" 2 import { Group, Group_GroupStatus } from "../../proto/qf/types_pb" 3 import { Color, getCourseID, hasEnrollments, isApprovedGroup, isPendingGroup } from "../Helpers" 4 import { useActions, useAppState } from "../overmind" 5 import Button, { ButtonType } from "./admin/Button" 6 import DynamicButton from "./DynamicButton" 7 import GroupForm from "./group/GroupForm" 8 import Search from "./Search" 9 10 11 /* Lists all groups for a given course. */ 12 const Groups = (): JSX.Element => { 13 const state = useAppState() 14 const actions = useActions() 15 const courseID = getCourseID() 16 17 const groupSearch = (group: Group) => { 18 // Show all groups if query is empty 19 if (state.query.length === 0) { 20 return false 21 } 22 23 // Show group if group name includes query 24 if (group.name.toLowerCase().includes(state.query)) { 25 return false 26 } 27 28 // Show group if any group user includes query 29 for (const user of group.users) { 30 if (user.Name.toLowerCase().includes(state.query)) { 31 return false 32 } 33 } 34 // Hide group if none of the above include query 35 return true 36 } 37 38 const GroupButtons = ({ group }: { group: Group }) => { 39 const buttons: JSX.Element[] = [] 40 if (isPendingGroup(group)) { 41 buttons.push( 42 <DynamicButton 43 text={"Approve"} 44 color={Color.BLUE} 45 type={ButtonType.BADGE} 46 onClick={() => actions.updateGroupStatus({ group, status: Group_GroupStatus.APPROVED })} 47 /> 48 ) 49 } 50 buttons.push( 51 <Button 52 text={"Edit"} 53 color={Color.YELLOW} 54 type={ButtonType.BADGE} 55 className="ml-2" 56 onClick={() => actions.setActiveGroup(group)} 57 /> 58 ) 59 buttons.push( 60 <DynamicButton 61 text={"Delete"} 62 color={Color.RED} 63 type={ButtonType.BADGE} 64 className="ml-2" 65 onClick={() => actions.deleteGroup(group)} 66 /> 67 ) 68 69 return <td className="d-flex">{buttons}</td> 70 } 71 72 const GroupMembers = ({ group }: { group: Group }) => { 73 if (!hasEnrollments(group)) { 74 return <td>No members</td> 75 } 76 77 const members = group.enrollments.map((enrollment, index) => { 78 return ( 79 <span key={enrollment.ID.toString()} className="inline-block"> 80 <a href={`https://github.com/${enrollment.user?.Login}`} target="_blank" rel="noopener noreferrer">{enrollment.user?.Name}</a> 81 {index >= group.enrollments.length - 1 ? "" : ", "} 82 </span> 83 ) 84 }) 85 return <td>{members}</td> 86 } 87 88 const GroupRow = ({ group }: { group: Group }) => { 89 return ( 90 <tr hidden={groupSearch(group)}> 91 <td key={group.ID.toString()}> 92 {group.name} 93 <span className="badge badge-warning ml-2">{isPendingGroup(group) ? "Pending" : null}</span> 94 </td> 95 <GroupMembers group={group} /> 96 <GroupButtons group={group} /> 97 </tr> 98 ) 99 } 100 101 // Generates JSX.Element array containing all groups for the course 102 const PendingGroups = state.groups[courseID.toString()]?.filter(group => isPendingGroup(group)).map(group => { 103 return <GroupRow key={group.ID.toString()} group={group} /> 104 }) 105 106 const ApprovedGroups = state.groups[courseID.toString()]?.filter(group => isApprovedGroup(group)).map(group => { 107 return <GroupRow key={group.ID.toString()} group={group} /> 108 }) 109 110 // If a group is active (being edited), show the group form 111 if (state.activeGroup) { 112 return <GroupForm /> 113 } 114 115 return ( 116 <div className="box"> 117 <div className="pb-2"> 118 <Search /> 119 </div> 120 <table className="table table-striped table-grp table-hover"> 121 <thead className="thead-dark"> 122 <th>Name</th> 123 <th>Members</th> 124 <th>Manage</th> 125 </thead> 126 <tbody> 127 {PendingGroups} 128 {ApprovedGroups} 129 </tbody> 130 </table> 131 </div> 132 ) 133 } 134 135 export default Groups