github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/public/src/components/group/GroupForm.tsx (about) 1 import React, { useEffect, useState } from "react" 2 import { Enrollment, Enrollment_UserStatus, Group } from "../../../proto/qf/types_pb" 3 import { Color, getCourseID, hasTeacher, isApprovedGroup, isHidden, isPending, isStudent } from "../../Helpers" 4 import { useActions, useAppState } from "../../overmind" 5 import Button, { ButtonType } from "../admin/Button" 6 import DynamicButton from "../DynamicButton" 7 import Search from "../Search" 8 9 10 const GroupForm = (): JSX.Element | null => { 11 const state = useAppState() 12 const actions = useActions() 13 14 const [query, setQuery] = useState<string>("") 15 const [enrollmentType, setEnrollmentType] = useState<Enrollment_UserStatus.STUDENT | Enrollment_UserStatus.TEACHER>(Enrollment_UserStatus.STUDENT) 16 const courseID = getCourseID() 17 18 const group = state.activeGroup 19 useEffect(() => { 20 if (isStudent(state.enrollmentsByCourseID[courseID.toString()])) { 21 actions.setActiveGroup(new Group()) 22 actions.updateGroupUsers(state.self.clone()) 23 } 24 return () => { 25 actions.setActiveGroup(null) 26 } 27 }, []) 28 if (!group) { 29 return null 30 } 31 const userIds = group.users.map(user => user.ID) 32 33 const search = (enrollment: Enrollment): boolean => { 34 if (userIds.includes(enrollment.userID) || enrollment.group && enrollment.groupID !== group.ID) { 35 return true 36 } 37 if (enrollment.user) { 38 return isHidden(enrollment.user.Name, query) 39 } 40 return false 41 } 42 43 const enrollments = state.courseEnrollments[courseID.toString()].map(enrollment => enrollment.clone()) 44 45 // Determine the user's enrollment status (teacher or student) 46 const isTeacher = hasTeacher(state.status[courseID.toString()]) 47 48 const enrollmentFilter = (enrollment: Enrollment) => { 49 if (isTeacher) { 50 // If the user is a teacher, show all enrollments of the selected enrollment type 51 return enrollment.status === enrollmentType 52 } 53 // Show all students 54 return enrollment.status === Enrollment_UserStatus.STUDENT 55 } 56 57 const groupFilter = (enrollment: Enrollment) => { 58 if (group && group.ID) { 59 // If a group is being edited, show users that are in the group 60 // This is to allow users to be removed from the group, and to be re-added 61 return enrollment.groupID === group.ID || enrollment.groupID === BigInt(0) 62 } 63 // Otherwise, show users that are not in a group 64 return enrollment.groupID === BigInt(0) 65 } 66 67 const sortedAndFilteredEnrollments = enrollments 68 // Filter enrollments where the user is not a student (or teacher), or the user is already in a group 69 .filter(enrollment => enrollmentFilter(enrollment) && groupFilter(enrollment)) 70 // Sort by name 71 .sort((a, b) => (a.user?.Name ?? "").localeCompare((b.user?.Name ?? ""))) 72 73 const AvailableUser = ({ enrollment }: { enrollment: Enrollment }) => { 74 const id = enrollment.userID 75 if (isPending(enrollment)) { 76 return null 77 } 78 if (id !== state.self.ID && !userIds.includes(id)) { 79 return ( 80 <li hidden={search(enrollment)} key={id.toString()} className="list-group-item"> 81 {enrollment.user?.Name} 82 <Button 83 text={"+"} 84 color={Color.GREEN} 85 type={ButtonType.BADGE} 86 className="ml-2 float-right" 87 onClick={() => actions.updateGroupUsers(enrollment.user)} 88 /> 89 </li> 90 ) 91 } 92 return null 93 } 94 95 const groupMembers = group.users.map(user => { 96 return ( 97 <li key={user.ID.toString()} className="list-group-item"> 98 <img id="group-image" src={user.AvatarURL} alt="" /> 99 {user.Name} 100 <Button 101 text={"-"} 102 color={Color.RED} 103 type={ButtonType.BADGE} 104 className="float-right" 105 onClick={() => actions.updateGroupUsers(user)} 106 /> 107 </li> 108 ) 109 }) 110 111 const toggleEnrollmentType = () => { 112 if (hasTeacher(enrollmentType)) { 113 setEnrollmentType(Enrollment_UserStatus.STUDENT) 114 } else { 115 setEnrollmentType(Enrollment_UserStatus.TEACHER) 116 } 117 } 118 119 const EnrollmentTypeButton = () => { 120 if (!isTeacher) { 121 return <div>Students</div> 122 } 123 return ( 124 <button className="btn btn-primary w-100" type="button" onClick={toggleEnrollmentType}> 125 {enrollmentType === Enrollment_UserStatus.STUDENT ? "Students" : "Teachers"} 126 </button> 127 ) 128 } 129 130 const GroupNameBanner = <div className="card-header" style={{ textAlign: "center" }}>{group.name}</div> 131 const GroupNameInput = group && isApprovedGroup(group) 132 ? null 133 : <input placeholder={"Group Name:"} onKeyUp={e => actions.updateGroupName(e.currentTarget.value)} /> 134 135 return ( 136 <div className="container"> 137 <div className="row"> 138 <div className="card well col-md-offset-2"> 139 <div className="card-header" style={{ textAlign: "center" }}> 140 <EnrollmentTypeButton /> 141 </div> 142 <Search placeholder={"Search"} setQuery={setQuery} /> 143 144 <ul className="list-group list-group-flush"> 145 {sortedAndFilteredEnrollments.map((enrollment, index) => { 146 return <AvailableUser key={index} enrollment={enrollment} /> 147 })} 148 </ul> 149 </div> 150 151 <div className='col'> 152 <div className="card well col-md-offset-2" > 153 {GroupNameBanner} 154 {GroupNameInput} 155 {groupMembers} 156 {group && group.ID ? 157 <div className="row justify-content-md-center"> 158 <DynamicButton 159 text={"Update"} 160 color={Color.BLUE} 161 type={ButtonType.BUTTON} 162 className="ml-2" 163 onClick={() => actions.updateGroup(group)} 164 /> 165 <Button 166 text={"Cancel"} 167 color={Color.RED} 168 type={ButtonType.OUTLINE} 169 className="ml-2" 170 onClick={() => actions.setActiveGroup(null)} 171 /> 172 </div> 173 : 174 <DynamicButton 175 text={"Create Group"} 176 color={Color.GREEN} 177 type={ButtonType.BUTTON} 178 onClick={() => actions.createGroup({ courseID, users: userIds, name: group.name })} 179 /> 180 } 181 </div> 182 </div> 183 </div> 184 </div > 185 ) 186 } 187 188 export default GroupForm