Skip to content
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

Add additional validation to various forms #465

Merged
merged 9 commits into from
Feb 29, 2024
87 changes: 78 additions & 9 deletions csm_web/frontend/src/components/course/CreateSectionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";

import { DAYS_OF_WEEK } from "../../utils/datetime";
import { useUserEmails } from "../../utils/queries/base";
Expand All @@ -7,6 +7,8 @@ import { Spacetime } from "../../utils/types";
import Modal from "../Modal";
import TimeInput from "../TimeInput";

import ExclamationCircle from "../../../static/frontend/img/exclamation-circle.svg";

const makeSpacetime = (): Spacetime => {
return { id: -1, duration: 0, dayOfWeek: 1, startTime: "", location: "" };
};
Expand Down Expand Up @@ -45,7 +47,21 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
/**
* Capacity for the new section.
*/
const [capacity, setCapacity] = useState<string>("");
const [capacity, setCapacity] = useState<number>(0);

/**
* Validation text; if empty string, no validation text is displayed.
*/
const [validationText, setValidationText] = useState<string>("");

/**
* Automatically re-validate form if there was a previous validation error.
*/
useEffect(() => {
if (validationText !== "") {
validateSectionForm();
}
}, [mentorEmail, spacetimes, description, capacity]);

/**
* Create a new empty spacetime for the new section.
Expand All @@ -55,6 +71,44 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
setSpacetimes(oldSpacetimes => [...oldSpacetimes, makeSpacetime()]);
};

/**
* Validate current spacetime values.
*/
const validateSectionForm = (): boolean => {
// all fields must be filled out
if (mentorEmail === null || mentorEmail.trim() === "") {
setValidationText("Mentor email must not be blank");
return false;
} else if (spacetimes.length === 0) {
setValidationText("Must have at least one section time");
return false;
} else if (isNaN(capacity) || capacity < 0) {
setValidationText("Capacity must be non-negative");
return false;
}

// validate spacetime fields
for (const spacetime of spacetimes) {
if (spacetime.location === null || spacetime.location === undefined || spacetime.location.trim() === "") {
setValidationText("All section locations must be specified");
return false;
} else if (spacetime.dayOfWeek <= 0) {
setValidationText("All section occurrences must have a specified day of week");
return false;
} else if (spacetime.startTime.trim() === "") {
setValidationText("All section occurrences must have a specified start time");
return false;
} else if (isNaN(spacetime.duration) || spacetime.duration <= 0) {
setValidationText("All section occurrences must have duration greater than 0");
return false;
}
}

// all valid
setValidationText("");
return true;
};

/**
* Handle the change of a form field.
*/
Expand All @@ -79,7 +133,7 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
setDescription(value);
break;
case "capacity":
setCapacity(value);
setCapacity(parseInt(value));
break;
default:
console.error("Unknown input name: " + name);
Expand All @@ -93,6 +147,12 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
*/
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>): void => {
event.preventDefault();

if (!validateSectionForm()) {
// don't do anything if invalid
return;
}

const data = {
mentorEmail,
spacetimes,
Expand All @@ -105,6 +165,9 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
onSuccess: () => {
closeModal();
reloadSections();
},
onError: () => {
setValidationText("Error occurred on create");
}
});
};
Expand Down Expand Up @@ -141,8 +204,8 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
type="number"
min="0"
inputMode="numeric"
pattern="[0-9]*"
value={capacity}
pattern="[0-9]+"
value={isNaN(capacity) ? "" : capacity.toString()}
onChange={e => handleChange(-1, "capacity", e.target.value)}
/>
</label>
Expand Down Expand Up @@ -180,11 +243,11 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
className="form-select"
onChange={e => handleChange(index, "dayOfWeek", e.target.value)}
name={`dayOfWeek|${index}`}
value={dayOfWeek}
value={dayOfWeek.toString()}
required
>
{[["---", ""], ...Array.from(DAYS_OF_WEEK)].map(([label, value]) => (
<option key={value} value={value} disabled={value === "---"}>
{[["---", -1], ...Array.from(DAYS_OF_WEEK)].map(([label, value]) => (
<option key={value} value={value} disabled={label === "---"}>
{label}
</option>
))}
Expand All @@ -206,7 +269,7 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
className="form-input"
type="number"
name={`duration|${index}`}
value={duration}
value={isNaN(duration) ? "" : duration.toString()}
min={0}
onChange={e => handleChange(index, "duration", e.target.value)}
/>
Expand All @@ -217,6 +280,12 @@ export const CreateSectionModal = ({ courseId, closeModal, reloadSections }: Cre
</div>
</form>
<div className="create-section-submit-container">
{validationText !== "" && (
<div className="create-section-validation-text-container">
<ExclamationCircle className="icon outline" />
<span className="create-section-validation-text">{validationText}</span>
</div>
)}
<button className="secondary-btn" id="add-occurence-btn" onClick={appendSpacetime}>
Add another occurence
</button>
Expand Down
58 changes: 48 additions & 10 deletions csm_web/frontend/src/components/section/MetaEditModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";

import { useSectionUpdateMutation } from "../../utils/queries/sections";
import Modal from "../Modal";

import ExclamationCircle from "../../../static/frontend/img/exclamation-circle.svg";

interface MetaEditModalProps {
sectionId: number;
closeModal: () => void;
Expand All @@ -18,19 +20,49 @@ export default function MetaEditModal({
}: MetaEditModalProps): React.ReactElement {
// use existing capacity and description as initial values
const [formState, setFormState] = useState({ capacity: capacity, description: description });
const [validationText, setValidationText] = useState("");

const sectionUpdateMutation = useSectionUpdateMutation(sectionId);

function handleChange({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) {
setFormState(prevFormState => ({ ...prevFormState, [name]: value }));
}
useEffect(() => {
if (validationText !== "") {
validateForm();
}
});

const handleChange = ({ target: { name, value } }: React.ChangeEvent<HTMLInputElement>) => {
if (name === "capacity") {
setFormState(prevFormState => ({ ...prevFormState, [name]: parseInt(value) }));
} else {
setFormState(prevFormState => ({ ...prevFormState, [name]: value }));
}
};

const validateForm = () => {
if (isNaN(formState.capacity) || formState.capacity < 0) {
setValidationText("Capacity must be non-negative");
return false;
}

function handleSubmit(event: React.MouseEvent<HTMLButtonElement>) {
setValidationText("");
return true;
};

const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
//TODO: Handle API Failure
sectionUpdateMutation.mutate(formState);
closeModal();
}

if (!validateForm()) {
// don't do anything if invalid
return;
}

sectionUpdateMutation.mutate(formState, {
onSuccess: closeModal,
onError: () => {
setValidationText("Error occurred on save");
}
});
};

return (
<Modal closeModal={closeModal}>
Expand All @@ -46,7 +78,7 @@ export default function MetaEditModal({
min="0"
inputMode="numeric"
pattern="[0-9]*"
value={formState.capacity}
value={isNaN(formState.capacity) ? "" : formState.capacity.toString()}
onChange={handleChange}
autoFocus
/>
Expand All @@ -62,6 +94,12 @@ export default function MetaEditModal({
/>
</label>
<div className="form-actions">
{validationText !== "" && (
<div className="spacetime-edit-form-validation-container">
<ExclamationCircle className="icon" />
<span className="spacetime-edit-form-validation-text">{validationText}</span>
</div>
)}
<button className="primary-btn" onClick={handleSubmit}>
Save
</button>
Expand Down
84 changes: 75 additions & 9 deletions csm_web/frontend/src/components/section/SpacetimeEditModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DateTime } from "luxon";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";

import { DAYS_OF_WEEK } from "../../utils/datetime";
import { useSpacetimeModifyMutation, useSpacetimeOverrideMutation } from "../../utils/queries/spacetime";
Expand All @@ -8,6 +8,8 @@ import LoadingSpinner from "../LoadingSpinner";
import Modal from "../Modal";
import TimeInput from "../TimeInput";

import ExclamationCircle from "../../../static/frontend/img/exclamation-circle.svg";

import "../../css/spacetime-edit.scss";

interface SpacetimeEditModalProps {
Expand All @@ -30,26 +32,84 @@ const SpaceTimeEditModal = ({
const [date, setDate] = useState<string>("");
const [mode, setMode] = useState<string>(prevLocation && prevLocation.startsWith("http") ? "virtual" : "inperson");
const [showSaveSpinner, setShowSaveSpinner] = useState<boolean>(false);
const [validationText, setValidationText] = useState<string>("");

const spacetimeModifyMutation = useSpacetimeModifyMutation(sectionId, spacetimeId);
const spacetimeOverrideMutation = useSpacetimeOverrideMutation(sectionId, spacetimeId);

useEffect(() => {
if (validationText !== "") {
validateSpacetime();
}
}, [location, day, time, date, isPermanent]);

/**
* Validate current spacetime values.
*/
const validateSpacetime = (): boolean => {
// validate spacetime fields
if (location === null || location === undefined || location.trim() === "") {
setValidationText("All section locations must be specified");
return false;
} else if (isPermanent && day <= 0) {
// only check this if it's for permanent changes
setValidationText("All section occurrences must have a specified day of week");
return false;
} else if (time === "") {
setValidationText("All section occurrences must have a specified start time");
return false;
}

if (!isPermanent && (date === null || date.trim() === "")) {
setValidationText("Section date to override must be specified");
return false;
}

// all valid
setValidationText("");
return true;
};

const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
//TODO: Handle API failure

if (!validateSpacetime()) {
// don't do anythinng if invalid
return;
}

setShowSaveSpinner(true);
isPermanent
? spacetimeModifyMutation.mutate({
if (isPermanent) {
spacetimeModifyMutation.mutate(
{
dayOfWeek: day,
location: location,
startTime: time
})
: spacetimeOverrideMutation.mutate({
},
{
onSuccess: closeModal,
onError: () => {
setValidationText("Error occurred on save");
setShowSaveSpinner(false);
}
}
);
} else {
spacetimeOverrideMutation.mutate(
{
location: location,
startTime: time,
date: date
});
closeModal();
},
{
onSuccess: closeModal,
onError: () => {
setValidationText("Error occurred on save");
setShowSaveSpinner(false);
}
}
);
}
};

const today = DateTime.now().toISODate()!;
Expand Down Expand Up @@ -113,7 +173,7 @@ const SpaceTimeEditModal = ({
disabled={!isPermanent}
value={isPermanent ? day : "---"}
>
{[["---", ""], ...Array.from(DAYS_OF_WEEK)].map(([label, value]) => (
{[["---", -1], ...Array.from(DAYS_OF_WEEK)].map(([label, value]) => (
<option key={value} value={value} disabled={value === "---"}>
{label}
</option>
Expand Down Expand Up @@ -186,6 +246,12 @@ const SpaceTimeEditModal = ({
)}
</div>
<div className="form-actions">
{validationText !== "" && (
<div className="spacetime-edit-form-validation-container">
<ExclamationCircle className="icon" />
<span className="spacetime-edit-form-validation-text">{validationText}</span>
</div>
)}
{showSaveSpinner ? (
<LoadingSpinner />
) : (
Expand Down
Loading
Loading