Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d140eb4
waitlisting backend code: waitlist student model, apis (view, add, an…
gabrielhan23 Sep 30, 2025
fe784c6
reverted docker compose
gabrielhan23 Sep 30, 2025
af5d7ed
added a count waitlist method
R1A2J Oct 21, 2025
4affe39
displaying active feature for waitlisted database in admin site
R1A2J Oct 21, 2025
260cfdc
showing only active students when we click on particular section from…
R1A2J Oct 21, 2025
481626c
update sections
dbswjd24 Oct 28, 2025
1dc9477
added a count waitlist method
R1A2J Oct 21, 2025
6a0b708
displaying active feature for waitlisted database in admin site
R1A2J Oct 21, 2025
bbaca01
showing only active students when we click on particular section from…
R1A2J Oct 21, 2025
97cf4a9
add max_waitlist_capacity to section partial_update
maxmwang Oct 28, 2025
86ddc72
Merge branch 'feat/waitlisting/integration' into feat/waitlisting/fro…
maxmwang Oct 28, 2025
61fb87c
add max_waitlist_capacity to section partial_update
maxmwang Oct 28, 2025
fc24af8
Merge branch 'feat/waitlisting/integration' into feat/waitlisting/fro…
maxmwang Oct 28, 2025
9a20d84
add waitlistCapacity to MetaEditModal and MentorSectionInfo
maxmwang Oct 28, 2025
0e95ea1
edit waitlistCapacity on frontend (#527)
maxmwang Oct 28, 2025
14ce137
add numWaitlistedStudnets to section serializer (#528)
maxmwang Oct 28, 2025
2362aae
add numWaitlistedStudnets to section serializer (#528)
maxmwang Oct 28, 2025
bb3707a
allow students to join waitlist for sections
maxmwang Nov 4, 2025
489118c
add view waitlist frontend and fix backend endpoint add
gabrielhan23 Nov 4, 2025
3ba529f
Merge branch 'feat/waitlisting/frontend' of github.com:csmberkeley/cs…
gabrielhan23 Nov 4, 2025
ff874a1
basic view, can add to waitlist, fixed card bugs
gabrielhan23 Nov 11, 2025
bbe7a3e
added waitlist section view
gabrielhan23 Nov 18, 2025
b1f3139
fix resources
gabrielhan23 Nov 18, 2025
46cbe03
add migrations
gabrielhan23 Sep 3, 2025
6e2c981
fix migration
gabrielhan23 Jan 30, 2026
3bd1e5c
add numWaitlistedStudnets to section serializer (#531)
gabrielhan23 Jan 30, 2026
a9ec880
merge
gabrielhan23 Jan 30, 2026
55691ff
fix waitlisting tests, allow cascade waitlist, add from coord, preven…
gabrielhan23 Jan 30, 2026
2be41d9
fix tests
gabrielhan23 Feb 4, 2026
5fc7cd0
fix backend for section swap
gabrielhan23 Feb 25, 2026
058a158
update frontend to show warning modals
gabrielhan23 Feb 25, 2026
914a282
add tests
gabrielhan23 Feb 25, 2026
2beb837
merge with main
gabrielhan23 Feb 25, 2026
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
2 changes: 1 addition & 1 deletion .babelrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-react", "@babel/preset-typescript"],
"plugins": ["transform-class-properties", "lodash", "@babel/plugin-transform-runtime", "dynamic-import-node"]
"plugins": ["transform-class-properties", "@babel/plugin-transform-runtime", "dynamic-import-node"]
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The lodash plugin was removed from the Babel configuration. If lodash is used anywhere in the frontend code, this could lead to larger bundle sizes as tree-shaking won't be as effective. Verify that lodash is not used in the codebase, or if it is, ensure that imports are done correctly (e.g., import map from 'lodash/map' instead of import { map } from 'lodash') to enable tree-shaking.

Suggested change
"plugins": ["transform-class-properties", "@babel/plugin-transform-runtime", "dynamic-import-node"]
"plugins": ["transform-class-properties", "@babel/plugin-transform-runtime", "lodash", "dynamic-import-node"]

Copilot uses AI. Check for mistakes.
}
6 changes: 5 additions & 1 deletion csm_web/frontend/src/components/course/Course.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):

let currDaySections = sections && sections[currDayGroup];
if (currDaySections && !showUnavailable) {
currDaySections = currDaySections.filter(({ numStudentsEnrolled, capacity }) => numStudentsEnrolled < capacity);
currDaySections = currDaySections.filter(
({ numStudentsEnrolled, capacity, numStudentsWaitlisted, waitlistCapacity }) =>
numStudentsEnrolled < capacity || numStudentsWaitlisted < waitlistCapacity
);
}

const enrollmentDate =
Expand Down Expand Up @@ -206,6 +209,7 @@ const Course = ({ courses, priorityEnrollment, enrollmentTimes }: CourseProps):
key={section.id}
userIsCoordinator={userIsCoordinator}
courseOpen={course.enrollmentOpen}
courseId={course.id}
{...section}
/>
))
Expand Down
122 changes: 107 additions & 15 deletions csm_web/frontend/src/components/course/SectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import React, { useState } from "react";
import { Link, Navigate } from "react-router-dom";

import { formatSpacetimeInterval } from "../../utils/datetime";
import { EnrollUserMutationResponse, useEnrollUserMutation } from "../../utils/queries/sections";
import { Mentor, Spacetime } from "../../utils/types";
import { useProfiles } from "../../utils/queries/base";
import {
EnrollUserMutationResponse,
useEnrollUserMutation,
useEnrollStudentToWaitlistMutation
} from "../../utils/queries/sections";
import { Mentor, Role, Spacetime } from "../../utils/types";
import Modal, { ModalCloser } from "../Modal";

import CheckCircle from "../../../static/frontend/img/check_circle.svg";
import ClockIcon from "../../../static/frontend/img/clock.svg";
import GroupIcon from "../../../static/frontend/img/group.svg";
import LocationIcon from "../../../static/frontend/img/location.svg";
import UserIcon from "../../../static/frontend/img/user.svg";
import WaitlistIcon from "../../../static/frontend/img/waitlist.svg";
import XCircle from "../../../static/frontend/img/x_circle.svg";

interface SectionCardProps {
Expand All @@ -22,6 +28,9 @@ interface SectionCardProps {
description: string;
userIsCoordinator: boolean;
courseOpen: boolean;
numStudentsWaitlisted: number;
waitlistCapacity: number;
courseId: number;
}

export const SectionCard = ({
Expand All @@ -32,12 +41,21 @@ export const SectionCard = ({
capacity,
description,
userIsCoordinator,
courseOpen
courseOpen,
numStudentsWaitlisted,
waitlistCapacity,
courseId
}: SectionCardProps): React.ReactElement => {
/**
* Mutation to enroll a student in the section.
*/
const enrollStudentMutation = useEnrollUserMutation(id);
/**
* Mutation to enroll a student in the section's waitlist.
*/
const enrollStudentWaitlistMutation = useEnrollStudentToWaitlistMutation(id);

const { data: profiles } = useProfiles();

/**
* Whether to show the modal (after an attempt to enroll).
Expand All @@ -51,19 +69,26 @@ export const SectionCard = ({
* The error message if the enrollment failed.
*/
const [errorMessage, setErrorMessage] = useState<string>("");
/**
* Whether to show the swap/waitlist confirmation modal.
*/
const [showSwapConfirm, setShowSwapConfirm] = useState<boolean>(false);
/**
* Whether the pending action is a waitlist join (vs direct enroll).
*/
const [pendingIsWaitlist, setPendingIsWaitlist] = useState<boolean>(false);

/**
* Handle enrollment in the section.
* Check if the user is already enrolled in another section of this course.
*/
const enroll = () => {
if (!courseOpen) {
setShowModal(true);
setEnrollmentSuccessful(false);
setErrorMessage("The course is not open for enrollment.");
return;
}
const isAlreadyEnrolled = profiles?.some(p => p.courseId === courseId && p.role === Role.STUDENT) ?? false;

enrollStudentMutation.mutate(undefined, {
/**
* Perform the actual mutation (enroll or waitlist).
*/
const performEnroll = (useWaitlist: boolean) => {
const mutation = useWaitlist ? enrollStudentWaitlistMutation : enrollStudentMutation;
mutation.mutate(undefined, {
onSuccess: () => {
setEnrollmentSuccessful(true);
setShowModal(true);
Expand All @@ -76,6 +101,31 @@ export const SectionCard = ({
});
};

/**
* Handle enrollment in the section.
*/
const enroll = () => {
if (!courseOpen) {
setShowModal(true);
setEnrollmentSuccessful(false);
setErrorMessage("The course is not open for enrollment.");
return;
}

// Determine if we should use waitlist mutation (enrolled capacity is full but waitlist is not full)
const isEnrolledFull = numStudentsEnrolled >= capacity;
const shouldUseWaitlist = isEnrolledFull && numStudentsWaitlisted < waitlistCapacity;

// If user is already enrolled in another section, show a confirmation warning
if (isAlreadyEnrolled) {
setPendingIsWaitlist(shouldUseWaitlist);
setShowSwapConfirm(true);
return;
}

performEnroll(shouldUseWaitlist);
};

/**
* Handle closeing of the modal.
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Typo in comment. "closeing" should be "closing".

Suggested change
* Handle closeing of the modal.
* Handle closing of the modal.

Copilot uses AI. Check for mistakes.
*/
Expand Down Expand Up @@ -114,7 +164,8 @@ export const SectionCard = ({

const iconWidth = "1.3em";
const iconHeight = "1.3em";
const isFull = numStudentsEnrolled >= capacity;
const isFull = numStudentsEnrolled >= capacity && numStudentsWaitlisted >= waitlistCapacity;
const isEnrolledFull = numStudentsEnrolled >= capacity;
if (!showModal && enrollmentSuccessful) {
// redirect to the section page if the user was successfully enrolled in the section
return <Navigate to="/" />;
Expand All @@ -130,6 +181,43 @@ export const SectionCard = ({

return (
<React.Fragment>
{showSwapConfirm && (
<Modal closeModal={() => setShowSwapConfirm(false)}>
<div className="enroll-confirm-modal-contents">
{pendingIsWaitlist ? (
<>
<h3>Join waitlist?</h3>
<p style={{ margin: "0.5em 1.5em", textAlign: "center" }}>
You are currently enrolled in another section of this course. When a spot opens up on this waitlist,
you will be <strong>automatically dropped</strong> from your current section and enrolled in this one.
</p>
</>
) : (
<>
<h3>Switch sections?</h3>
<p style={{ margin: "0.5em 1.5em", textAlign: "center" }}>
You are currently enrolled in another section of this course. Enrolling here will{" "}
<strong>drop you from your current section</strong> and enroll you in this one.
</p>
</>
)}
<div style={{ display: "flex", gap: "1em", marginTop: "1em" }}>
<button className="secondary-btn" onClick={() => setShowSwapConfirm(false)}>
Cancel
</button>
<button
className="primary-btn"
onClick={() => {
setShowSwapConfirm(false);
performEnroll(pendingIsWaitlist);
}}
>
Confirm
</button>
</div>
</div>
</Modal>
)}
{showModal && <Modal closeModal={closeModal}>{modalContents()}</Modal>}
<section className={`section-card ${isFull ? "full" : ""}`}>
<div className="section-card-contents">
Expand Down Expand Up @@ -171,7 +259,11 @@ export const SectionCard = ({
<UserIcon width={iconWidth} height={iconHeight} /> {mentor.name}
</p>
<p title="Current enrollment">
<GroupIcon width={iconWidth} height={iconHeight} /> {`${numStudentsEnrolled}/${capacity}`}
<GroupIcon width={iconWidth} height={iconHeight} /> {`Enrolled: ${numStudentsEnrolled}/${capacity}`}
</p>
<p title="Current waitlist">
<WaitlistIcon width={iconWidth} height={iconHeight} />{" "}
{`Waitlisted: ${numStudentsWaitlisted}/${waitlistCapacity}`}
</p>
</div>
{userIsCoordinator ? (
Expand All @@ -184,7 +276,7 @@ export const SectionCard = ({
disabled={!courseOpen || isFull}
onClick={isFull ? undefined : enroll}
>
ENROLL
{isFull ? "FULL" : isEnrolledFull ? "JOIN WAITLIST" : "ENROLL"}
</button>
)}
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Link } from "react-router-dom";

import { useUserEmails } from "../../utils/queries/base";
import { useEnrollStudentMutation } from "../../utils/queries/sections";
import { useEnrollStudentMutation, useCoordEnrollStudentToWaitlistMutation } from "../../utils/queries/sections";
import LoadingSpinner from "../LoadingSpinner";
import Modal from "../Modal";

Expand All @@ -22,6 +22,10 @@
interface CoordinatorAddStudentModalProps {
closeModal: (arg0?: boolean) => void;
sectionId: number;
title: string;
mutation: (
sectionId: number
) => ReturnType<typeof useEnrollStudentMutation> | ReturnType<typeof useCoordEnrollStudentToWaitlistMutation>;
}

interface RequestType {
Expand All @@ -39,7 +43,7 @@
progress?: Array<{
email: string;
status: string;
detail?: any;

Check warning on line 46 in csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx

View workflow job for this annotation

GitHub Actions / ESLint

csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx#L46

Unexpected any. Specify a different type (@typescript-eslint/no-explicit-any)
}>;
}

Expand All @@ -50,10 +54,12 @@

export function CoordinatorAddStudentModal({
closeModal,
sectionId
sectionId,
title,
mutation
}: CoordinatorAddStudentModalProps): React.ReactElement {
const { data: userEmails, isSuccess: userEmailsLoaded } = useUserEmails();
const enrollStudentMutation = useEnrollStudentMutation(sectionId);
const enrollMutation = mutation(sectionId);

const [emailsToAdd, setEmailsToAdd] = useState<string[]>([""]);
const [response, setResponse] = useState<ResponseType>({} as ResponseType);
Expand Down Expand Up @@ -127,8 +133,8 @@
request.actions["capacity"] = responseActions.get("capacity") as string;
}

enrollStudentMutation.mutate(request, {
onError: ({ status, json }) => {
enrollMutation.mutate(request, {
onError: ({ status, json }: { status: number; json: any }) => {

Check warning on line 137 in csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx

View workflow job for this annotation

GitHub Actions / ESLint

csm_web/frontend/src/components/section/CoordinatorAddStudentModal.tsx#L137

Unexpected any. Specify a different type (@typescript-eslint/no-explicit-any)
if (status === 500) {
// internal error
setResponse({
Expand Down Expand Up @@ -182,7 +188,7 @@

const initial_component = (
<React.Fragment>
<h2>Add new students</h2>
<h2>Add new {title}</h2>
<div className="coordinator-email-content">
<div className="coordinator-email-input-list">
{emailsToAdd.map((email, index) => (
Expand Down Expand Up @@ -297,6 +303,9 @@
conflictDetail = "User is already a coordinator for the course!";
} else if (email_obj.detail.reason === "mentor") {
conflictDetail = "User is already a mentor for the course!";
} else {
// display the reason string directly (e.g. from waitlist coord-add)
conflictDetail = email_obj.detail.reason;
}
drop_disabled = true;
} else if (email_obj.detail.section.id == sectionId) {
Expand Down
5 changes: 4 additions & 1 deletion csm_web/frontend/src/components/section/MentorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface MentorSectionProps {
capacity: number;
description: string;
courseRestricted: boolean;
waitlistCapacity: number;
}

export default function MentorSection({
Expand All @@ -28,7 +29,8 @@ export default function MentorSection({
capacity,
description,
userRole,
mentor
mentor,
waitlistCapacity
}: MentorSectionProps) {
return (
<SectionDetail
Expand All @@ -55,6 +57,7 @@ export default function MentorSection({
description={description}
id={id}
courseRestricted={courseRestricted}
waitlistCapacity={waitlistCapacity}
/>
}
/>
Expand Down
Loading
Loading