Skip to content

Commit

Permalink
Add additional validation to various forms (#465)
Browse files Browse the repository at this point in the history
  • Loading branch information
smartspot2 authored Feb 29, 2024
1 parent 49b0dca commit c747328
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 75 deletions.
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

0 comments on commit c747328

Please sign in to comment.