Skip to content

Comments

Revert "Revert "Feat/refactor form""#1232

Open
rlho wants to merge 6 commits intomainfrom
revert-1231-revert-1138-feat/refactor-form
Open

Revert "Revert "Feat/refactor form""#1232
rlho wants to merge 6 commits intomainfrom
revert-1231-revert-1138-feat/refactor-form

Conversation

@rlho
Copy link
Collaborator

@rlho rlho commented Feb 20, 2026

Reverts #1231

@nopivnick
Copy link
Member

@n3xta

the original PR had some bugs that need to be addressed:

  1. the secondary contact email field is validating netIDs (i.e., abc123) but it needs to validate nyu netID emails (i.e., abc123@nyu.edu).

  2. the secondary contact is being invited to the approved calendar event however they are not receiving an approved email notification.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR reverts a previous revert (PR #1231), effectively re-introducing a comprehensive form refactoring that modernizes the booking form's sponsor and secondary contact handling. The changes improve data collection by splitting names into first/last components, standardize email input formats, and add automatic security requirements for large events.

Changes:

  • Refactored sponsor email field to accept Net ID format only (e.g., "abc123") instead of full email addresses
  • Split secondary contact name into separate first/last name fields and added secondary contact email field with full NYU email validation
  • Added automatic security requirement for events with 75+ attendees with manual override tracking

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
booking-app/tests/unit/form-context-field-visibility.unit.test.tsx Updated test expectations for new sponsor email label format
booking-app/tests/unit/calendar-description.unit.test.ts Added "en-US" locale to date/time formatting for consistency
booking-app/tests/unit/MoreInfoModal.basic.unit.test.tsx Updated expected count of "none" values to account for new secondary email field
booking-app/components/src/utils/validationHelpers.ts New utility with centralized Net ID and NYU email regex patterns and validation functions
booking-app/components/src/utils/formatters.ts Added getSecondaryContactName helper for backwards-compatible name handling
booking-app/components/src/types.ts Updated Inputs type to split secondaryName into firstName/lastName/email fields
booking-app/components/src/server/db.ts Standardized date/time formatting with explicit "en-US" locale
booking-app/components/src/server/calendars.ts Updated calendar description to include secondary contact email and use new helper
booking-app/components/src/server/admin.ts Added secondary contact email sending in approval flow with legacy format handling
booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx Updated modal to display split secondary contact fields and new email field
booking-app/components/src/client/routes/booking/components/FormInput.tsx Major refactor: sponsor email to Net ID, secondary contact split, auto-security logic, ChartField validation
booking-app/components/src/client/routes/booking/components/BookingFormInputs.tsx Enhanced validation logic for optional fields and added disabled prop support
booking-app/app/templates/booking_detail.html Updated email template to use secondaryContactName helper and display secondary email
booking-app/app/api/bookings/route.ts Standardized date/time formatting and added secondaryContactName to email context
booking-app/app/api/bookings/edit/route.ts Added secondaryContactName to edit notification email context

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 154 to 156
secondaryFirstName: string;
secondaryLastName: string;
secondaryEmail: string;
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The secondaryName field is marked as optional (secondaryName?: string) for backwards compatibility, but the new fields secondaryFirstName, secondaryLastName, and secondaryEmail are required (no ? operator). This creates an inconsistency where old bookings have secondaryName and new bookings have the split fields. Consider making the new fields optional as well to better represent the data model, or add a comment explaining that these fields are mutually exclusive.

Suggested change
secondaryFirstName: string;
secondaryLastName: string;
secondaryEmail: string;
// Secondary attendee: legacy data may use `secondaryName` only; newer flows use the split fields.
secondaryFirstName?: string;
secondaryLastName?: string;
secondaryEmail?: string;

Copilot uses AI. Check for mistakes.
Comment on lines 777 to 787
<BookingFormTextField
id="sponsorEmail"
label="Sponsor Email"
description="Must be an nyu.edu email address."
label="Sponsor Email (NYU Net ID)"
description="Enter the sponsor's NYU Net ID (e.g., abc123)"
required={watch("role") === Role.STUDENT}
pattern={{
value: /^[A-Z0-9._%+-]+@nyu.edu$/i,
message: "Invalid email address",
value: NET_ID_REGEX,
message: "Invalid Net ID",
}}
validate={validateSponsorEmailSimple}
validate={validateSponsorNetIdSimple}
{...{ control, errors, trigger }}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The sponsor email field now accepts Net ID format only (e.g., "abc123"), but the email is sent to the stored value without transformation to a full email address in the serverApproveEvent function. Unlike secondaryEmail which has legacy format handling at lines 677-679 in admin.ts, sponsorEmail is used directly at line 663. This means sponsor emails will be sent to invalid addresses like "abc123" instead of "abc123@nyu.edu". The sponsor email needs transformation logic similar to secondaryEmail when approval emails are sent.

Copilot uses AI. Check for mistakes.
Comment on lines 658 to 670
// for sponsor, if we have one
const contents = await serverBookingContents(id, tenant);
if (contents.role === "Student" && contents.sponsorEmail?.length > 0) {
serverSendBookingDetailEmail({
calendarEventId: id,
targetEmail: contents.sponsorEmail,
headerMessage:
"A reservation that you are the Sponsor of has been approved.<br /><br />" +
emailConfig.emailMessages.approvalNotice,
status: BookingStatusLabel.APPROVED,
replyTo: guestEmail,
tenant,
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The sponsor email is being used directly without transformation from Net ID format to full email format. Since the form now accepts Net IDs only (not full emails), this code will attempt to send emails to invalid addresses like "abc123" instead of "abc123@nyu.edu". Add transformation logic to construct the full email address before sending, similar to how secondaryEmail is handled at lines 677-679.

Copilot uses AI. Check for mistakes.
Comment on lines 226 to 269
const expectedAttendanceValue = watch("expectedAttendance");
const isLargeEvent = parseInt(expectedAttendanceValue || "0") >= 75;

const hireSecurityValue = watch("hireSecurity");
// Track if hireSecurity was auto-set by attendance logic
const hireSecurityWasAutoSet = useRef(false);
// Track the last value we set automatically (e.g. "yes" or "")
const autoHireSecurityValueRef = useRef<string | undefined>(undefined);
// Track whether the user has manually overridden hireSecurity
const hireSecurityManuallySet = useRef(false);

// Detect manual changes to hireSecurity by comparing against the last auto-set value
useEffect(() => {
// If we have an auto baseline, and the current value differs, treat as manual override
if (
autoHireSecurityValueRef.current !== undefined &&
hireSecurityValue !== autoHireSecurityValueRef.current
) {
hireSecurityManuallySet.current = true;
hireSecurityWasAutoSet.current = false;
autoHireSecurityValueRef.current = undefined;
}
}, [hireSecurityValue]);

useEffect(() => {
// Do not auto-manage hireSecurity if the user has manually overridden it
if (hireSecurityManuallySet.current) {
return;
}
if (isLargeEvent) {
if (hireSecurityValue !== "yes") {
setValue("hireSecurity", "yes", { shouldValidate: true });
hireSecurityWasAutoSet.current = true;
autoHireSecurityValueRef.current = "yes";
}
} else {
// Only reset if it was auto-set previously
if (hireSecurityWasAutoSet.current && hireSecurityValue !== "") {
setValue("hireSecurity", "", { shouldValidate: true });
hireSecurityWasAutoSet.current = false;
autoHireSecurityValueRef.current = "";
}
}
}, [isLargeEvent, hireSecurityValue, setValue]);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The auto-setting logic for hireSecurity could cause issues if expectedAttendance is changed from above 75 to below 75 and then back above 75. Once the user manually changes hireSecurity (line 244), the hireSecurityManuallySet flag is set to true and never reset. This means if attendance goes from 80 -> 70 -> 85, hireSecurity won't auto-set to "yes" on the second threshold crossing since it's still marked as manually set. Consider resetting hireSecurityManuallySet when expectedAttendance crosses the threshold in the auto-enabling direction.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@n3xta Can you check the comment?

Comment on lines 156 to 174
validate: (value) => {
if (!required) return true;
const isNotEmpty = value?.trim().length > 0;
const isValid = validate(value);
return (isNotEmpty && isValid) || `${label} is required`;
// If field is not required and empty, allow it
if (!required && (!value || value.trim().length === 0)) return true;

// Check for whitespace-only values
if (value && typeof value === "string" && value.trim().length === 0) {
return `${label} cannot be empty whitespace`;
}

// For required fields, check if value is valid
if (required) {
const isNotEmpty = value?.trim().length > 0;
const isValid = validate(value);
return (isNotEmpty && isValid) || `${label} is required`;
}

// For optional fields with a value, run custom validation
return validate(value);
},
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The validate function will be called with the value even for optional fields with values (line 173), but the validate parameter defaults to returning true (line 147). This means if a custom validation function expects a non-empty value but receives an empty string for an optional field, it should handle that case. Consider adding a comment to clarify that custom validate functions for optional fields should handle empty values, or ensure empty values aren't passed to custom validators for optional fields.

Copilot uses AI. Check for mistakes.
Comment on lines +698 to +704
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/inviteUser`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(secondaryFormData),
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The fetch to /api/inviteUser for the secondary contact is awaited but doesn't have error handling. If the invite fails, the error will propagate and potentially stop the approval process. While it's acceptable to fail the approval if email delivery fails, the error message won't be informative. Consider adding try-catch with a specific error message, or at minimum add a comment explaining the intentional error propagation behavior.

Suggested change
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/inviteUser`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(secondaryFormData),
});
try {
const inviteResponse = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/inviteUser`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(secondaryFormData),
}
);
if (!inviteResponse.ok) {
throw new Error(
`Failed to invite secondary contact (${secondaryEmailAddress}); received status ${inviteResponse.status}.`
);
}
} catch (error) {
// Intentionally propagate a descriptive error so the approval failure is informative
const message =
error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to invite secondary contact (${secondaryEmailAddress}): ${message}`
);
}

Copilot uses AI. Check for mistakes.
label="ChartField for Room Setup"
required={false}
pattern={{
value: /^.{5}-.{2}-.{5}-.{5}$/,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The ChartField validation pattern uses /^.{5}-.{2}-.{5}-.{5}$/ which accepts ANY 5 characters (including spaces or special characters) in each segment. This is overly permissive. Consider using a more specific pattern like /^[A-Z0-9]{5}-[A-Z0-9]{2}-[A-Z0-9]{5}-[A-Z0-9]{5}$/ if ChartFields should only contain alphanumeric characters, or document the expected format more clearly if the current pattern is intentional.

Suggested change
value: /^.{5}-.{2}-.{5}-.{5}$/,
value: /^[A-Z0-9]{5}-[A-Z0-9]{2}-[A-Z0-9]{5}-[A-Z0-9]{5}$/,

Copilot uses AI. Check for mistakes.
@@ -319,12 +319,12 @@ describe("Calendar Description Functions", () => {
...data,
roomId: selectedRoomIds,
startDate: startDateObj.toLocaleDateString(),
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The test function uses toLocaleDateString() without specifying a locale at line 321, but the actual production code in booking-app/app/api/bookings/route.ts:195 uses toLocaleDateString("en-US"). For consistency and to ensure the test accurately reflects production behavior, add the "en-US" locale parameter here as well.

Copilot uses AI. Check for mistakes.
…e secondary contact fields for compatibility
…otifications to attendees

- Added sendUpdates: 'all' to insertEvent() when creating calendar events
- Added sendUpdates: 'all' to patchCalendarEvent() when updating events (e.g., adding secondary contacts)
- Added sendUpdates: 'all' to deleteEvent() when deleting events

This ensures that secondary contacts and other attendees receive Google Calendar notifications when they are added to bookings, when events are updated, or when bookings are cancelled.
- Updated 3 tests in server-calendars.unit.test.ts to expect sendUpdates: 'all' parameter
- Added new test 'deleteEvent sends cancellation notifications to all attendees'
- Fixed date locale issue in calendar-description.unit.test.ts (toLocaleDateString without locale)
@n3xta
Copy link
Collaborator

n3xta commented Feb 24, 2026

  • I checked for existing implementations and confirmed there is no duplication
  • I tested this feature locally
  • I had Copilot review the PR and incorporated feedback (or explained why not)
  • I confirmed there are no conflicts
  • I confirmed my PR passed all tests
  • I added or updated unit tests (or explained why not)
  • I attached screenshots or a video demonstrating the feature
  • I requested a code review from at least one other teammate

Screenshots

Fix to "the secondary contact email field is validating netIDs (i.e., abc123) but it needs to validate nyu netID emails (i.e., abc123@nyu.edu)."

image image

Fix to "the secondary contact is being invited to the approved calendar event however they are not receiving an approved email notification."

Notes:

Add sendUpdates parameter to Google Calendar API calls to send notifications to attendees

  • Added sendUpdates: 'all' to insertEvent() when creating calendar events
  • Added sendUpdates: 'all' to patchCalendarEvent() when updating events (e.g., adding secondary contacts)
  • Added sendUpdates: 'all' to deleteEvent() when deleting events
    This ensures that secondary contacts and other attendees receive Google Calendar notifications when they are added to bookings, when events are updated, or when bookings are cancelled.
image image 4764cfd1bd574cef6e186dcbbb9e8599 32bba7af51a91beb3986814d43244c2d

@nopivnick
Copy link
Member

@n3xta I see in your screenshots the Google Calendar invite email but did you confirm the Secondary Contact is receiving an APPROVED email notification from the booking app?

@n3xta
Copy link
Collaborator

n3xta commented Feb 24, 2026 via email

// Handle both legacy full email format and new Net ID format
const sponsorEmailAddress = contents.sponsorEmail.includes("@")
? contents.sponsorEmail
: `${contents.sponsorEmail}@nyu.edu`;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@n3xta
sponsorEmail field stores an email address, so why did you add this line?

* @param value - The string to validate
* @returns true if the value matches NYU email format, false otherwise
*/
export const isValidNyuEmailFormat = (value: string): boolean => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Where do you use isValidNyuEmailFormat ?

Comment on lines 226 to 269
const expectedAttendanceValue = watch("expectedAttendance");
const isLargeEvent = parseInt(expectedAttendanceValue || "0") >= 75;

const hireSecurityValue = watch("hireSecurity");
// Track if hireSecurity was auto-set by attendance logic
const hireSecurityWasAutoSet = useRef(false);
// Track the last value we set automatically (e.g. "yes" or "")
const autoHireSecurityValueRef = useRef<string | undefined>(undefined);
// Track whether the user has manually overridden hireSecurity
const hireSecurityManuallySet = useRef(false);

// Detect manual changes to hireSecurity by comparing against the last auto-set value
useEffect(() => {
// If we have an auto baseline, and the current value differs, treat as manual override
if (
autoHireSecurityValueRef.current !== undefined &&
hireSecurityValue !== autoHireSecurityValueRef.current
) {
hireSecurityManuallySet.current = true;
hireSecurityWasAutoSet.current = false;
autoHireSecurityValueRef.current = undefined;
}
}, [hireSecurityValue]);

useEffect(() => {
// Do not auto-manage hireSecurity if the user has manually overridden it
if (hireSecurityManuallySet.current) {
return;
}
if (isLargeEvent) {
if (hireSecurityValue !== "yes") {
setValue("hireSecurity", "yes", { shouldValidate: true });
hireSecurityWasAutoSet.current = true;
autoHireSecurityValueRef.current = "yes";
}
} else {
// Only reset if it was auto-set previously
if (hireSecurityWasAutoSet.current && hireSecurityValue !== "") {
setValue("hireSecurity", "", { shouldValidate: true });
hireSecurityWasAutoSet.current = false;
autoHireSecurityValueRef.current = "";
}
}
}, [isLargeEvent, hireSecurityValue, setValue]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@n3xta Can you check the comment?

@@ -693,14 +785,14 @@ export default function FormInput({
/>
<BookingFormTextField
id="sponsorEmail"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please don’t use the same column to store different types of data (like an email address and a NetID).
If you want to keep using this column as is, please convert it to an email format before saving it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants