Skip to content

Commit 33ee4a7

Browse files
authored
Ax/scrum 146 fix same-name-timetable frontend bug (#100)
Co-authored-by: Austin-X <[email protected]>
1 parent 13cc5f8 commit 33ee4a7

File tree

9 files changed

+118
-7
lines changed

9 files changed

+118
-7
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Format the code
22

33
on:
4+
workflow_dispatch:
45
push:
56

67
jobs:

.github/workflows/testing.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Run Tests
2-
on: push
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
37
jobs:
48
build:
59
runs-on: ubuntu-latest

course-matrix/backend/__tests__/timetablesController.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ jest.mock("../src/db/setupDb", () => ({
139139
) {
140140
return {
141141
eq: jest.fn().mockReturnThis(), // Allow further chaining of eq if required
142+
neq: jest.fn().mockImplementation(() => ({
143+
maybeSingle: jest
144+
.fn()
145+
.mockImplementation(() => ({ data: null, error: null })),
146+
})),
142147
maybeSingle: jest.fn().mockImplementation(() => {
143148
return { data: mockTimetables1, error: null };
144149
}),

course-matrix/backend/src/controllers/timetablesController.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,27 @@ export default {
190190
return res.status(404).json({ error: "Calendar id not found" });
191191
}
192192

193+
// Check if another timetable with the same title already exist for this user
194+
const { data: existingTimetable, error: existingTimetableError } =
195+
await supabase
196+
.schema("timetable")
197+
.from("timetables")
198+
.select("id")
199+
.eq("user_id", user_id)
200+
.eq("timetable_title", timetable_title)
201+
.neq("id", id)
202+
.maybeSingle();
203+
204+
if (existingTimetableError) {
205+
return res.status(400).json({ error: existingTimetableError.message });
206+
}
207+
208+
if (existingTimetable) {
209+
return res
210+
.status(400)
211+
.json({ error: "Another timetable with this title already exists" });
212+
}
213+
193214
let updateData: any = {};
194215
if (timetable_title) updateData.timetable_title = timetable_title;
195216
if (semester) updateData.semester = semester;

course-matrix/frontend/src/pages/Home/Home.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import TimetableCard from "./TimetableCard";
44
import TimetableCompareButton from "./TimetableCompareButton";
55
import TimetableCreateNewButton from "./TimetableCreateNewButton";
66
import { useGetTimetablesQuery } from "../../api/timetableApiSlice";
7+
import { useState } from "react";
8+
import TimetableErrorDialog from "../TimetableBuilder/TimetableErrorDialog";
79

810
export interface Timetable {
911
id: number;
@@ -31,13 +33,19 @@ const Home = () => {
3133
refetch: () => void;
3234
};
3335

36+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
37+
3438
return (
3539
<div className="w-full">
3640
<div className="m-8">
3741
<div className="mb-4 flex items-center gap-2 relative group">
3842
<h1 className="text-2xl font-medium tracking-tight">My Timetables</h1>
3943
<Pin size={24} className="text-blue-500" />
4044
</div>
45+
<TimetableErrorDialog
46+
errorMessage={errorMessage}
47+
setErrorMessage={setErrorMessage}
48+
/>
4149
<div className="mb-4 flex flex-row justify-between items-center">
4250
<div className="flex gap-4">
4351
<Button
@@ -78,6 +86,7 @@ const Home = () => {
7886
.map((timetable) => (
7987
<TimetableCard
8088
refetch={refetch}
89+
setErrorMessage={setErrorMessage}
8190
key={timetable.id}
8291
timetableId={timetable.id}
8392
title={timetable.timetable_title}

course-matrix/frontend/src/pages/Home/TimetableCard.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Link } from "react-router-dom";
1515

1616
interface TimetableCardProps {
1717
refetch: () => void;
18+
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
1819
timetableId: number;
1920
title: string;
2021
lastEditedDate: Date;
@@ -28,6 +29,7 @@ interface TimetableCardProps {
2829
*/
2930
const TimetableCard = ({
3031
refetch,
32+
setErrorMessage,
3133
timetableId,
3234
title,
3335
lastEditedDate,
@@ -56,7 +58,9 @@ const TimetableCard = ({
5658
}).unwrap();
5759
setIsEditingTitle(false);
5860
} catch (error) {
59-
console.error("Failed to update timetable title:", error);
61+
const errorData = (error as { data?: { error?: string } }).data;
62+
setErrorMessage(errorData?.error ?? "Unknown error occurred");
63+
return;
6064
}
6165
};
6266

course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
useDeleteRestrictionMutation,
3636
} from "@/api/restrictionsApiSlice";
3737
import { z } from "zod";
38-
import React, { useEffect, useRef } from "react";
38+
import React, { useEffect, useRef, useState } from "react";
3939
import { useGetNumberOfCourseSectionsQuery } from "@/api/coursesApiSlice";
4040
import {
4141
useCreateEventMutation,
@@ -58,6 +58,7 @@ import {
5858
getSemesterStartAndEndDatesPlusOneWeek,
5959
} from "@/utils/semester-utils";
6060
import { courseEventStyles } from "@/constants/calendarConstants";
61+
import TimetableErrorDialog from "./TimetableErrorDialog";
6162

6263
interface CalendarProps {
6364
setShowLoadingPage: React.Dispatch<React.SetStateAction<boolean>>;
@@ -110,6 +111,8 @@ const Calendar = React.memo<CalendarProps>(
110111
const [createRestriction] = useCreateRestrictionMutation();
111112
const [deleteRestriction] = useDeleteRestrictionMutation();
112113

114+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
115+
113116
const semesterStartDate = getSemesterStartAndEndDates(semester).start;
114117
const { start: semesterStartDatePlusOneWeek, end: semesterEndDate } =
115118
getSemesterStartAndEndDatesPlusOneWeek(semester);
@@ -224,17 +227,18 @@ const Calendar = React.memo<CalendarProps>(
224227
}, [timetablesData, editingTimetableId, isEditingTimetable]);
225228

226229
const handleCreate = async () => {
227-
setShowLoadingPage(true);
228230
const timetableTitle = timetableTitleRef.current?.value ?? "";
229231
// Create timetable
230232
const { data, error } = await createTimetable({
231233
timetable_title: timetableTitle,
232234
semester: semester,
233235
});
234236
if (error) {
235-
console.error(error);
237+
const errorData = (error as { data?: { error?: string } }).data;
238+
setErrorMessage(errorData?.error ?? "Unknown error occurred");
236239
return;
237240
}
241+
setShowLoadingPage(true);
238242
// Create course events for the newly created timetable
239243
const newTimetableId = data[0].id;
240244
for (const offeringId of newOfferingIds) {
@@ -336,6 +340,10 @@ const Calendar = React.memo<CalendarProps>(
336340
<div>
337341
<h1 className="text-2xl flex flex-row justify-between font-medium tracking-tight mb-8">
338342
<div>Your Timetable </div>
343+
<TimetableErrorDialog
344+
errorMessage={errorMessage}
345+
setErrorMessage={setErrorMessage}
346+
/>
339347
{!isEditingTimetable ? (
340348
<Dialog>
341349
{isChoosingSectionsManually &&

course-matrix/frontend/src/pages/TimetableBuilder/GeneratedCalendars.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { useCreateTimetableMutation } from "@/api/timetableApiSlice";
4545
import { useCreateEventMutation } from "@/api/eventsApiSlice";
4646
import { useCreateRestrictionMutation } from "@/api/restrictionsApiSlice";
4747
import { TimetableForm } from "@/models/timetable-form";
48+
import { set } from "zod";
49+
import TimetableErrorDialog from "./TimetableErrorDialog";
4850

4951
interface GeneratedCalendarsProps {
5052
setShowLoadingPage: (_: boolean) => void;
@@ -120,18 +122,21 @@ export const GeneratedCalendars = React.memo<GeneratedCalendarsProps>(
120122
parseEvent(index + 1, event, "courseEvent"),
121123
) ?? [];
122124

125+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
126+
123127
const handleCreate = async () => {
124-
setShowLoadingPage(true);
125128
const timetableTitle = timetableTitleRef.current?.value ?? "";
126129
// Create timetable
127130
const { data, error } = await createTimetable({
128131
timetable_title: timetableTitle,
129132
semester: semester,
130133
});
131134
if (error) {
132-
console.error(error);
135+
const errorData = (error as { data?: { error?: string } }).data;
136+
setErrorMessage(errorData?.error ?? "Unknown error occurred");
133137
return;
134138
}
139+
setShowLoadingPage(true);
135140
// Create course events for the newly created timetable
136141
const newTimetableId = data[0].id;
137142
for (const offering of currentTimetableOfferings) {
@@ -210,6 +215,10 @@ export const GeneratedCalendars = React.memo<GeneratedCalendarsProps>(
210215
>
211216
Cancel Generating
212217
</Button>
218+
<TimetableErrorDialog
219+
errorMessage={errorMessage}
220+
setErrorMessage={setErrorMessage}
221+
/>
213222
<Dialog>
214223
<DialogTrigger asChild>
215224
<Button size="sm" onClick={() => {}}>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
DialogHeader,
4+
DialogFooter,
5+
Dialog,
6+
DialogContent,
7+
DialogTitle,
8+
DialogDescription,
9+
DialogClose,
10+
} from "@/components/ui/dialog";
11+
import React from "react";
12+
13+
interface TimetableErrorDialogProps {
14+
errorMessage: string | null;
15+
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
16+
}
17+
18+
const TimetableErrorDialog: React.FC<TimetableErrorDialogProps> = ({
19+
errorMessage,
20+
setErrorMessage,
21+
}) => {
22+
const dialogTitle =
23+
errorMessage !== null ? errorMessage : "Unknown error occurred";
24+
const dialogDescription = errorMessage?.includes("title already exists")
25+
? "Please choose another title for your timetable"
26+
: null;
27+
28+
return (
29+
<Dialog
30+
open={errorMessage !== null}
31+
onOpenChange={() => setErrorMessage(null)}
32+
>
33+
<DialogContent className="gap-5">
34+
<DialogHeader>
35+
<DialogTitle className="text-red-500">{dialogTitle}</DialogTitle>
36+
<DialogDescription>{dialogDescription}</DialogDescription>
37+
</DialogHeader>
38+
<DialogFooter>
39+
<DialogClose asChild>
40+
<Button variant="secondary" onClick={() => setErrorMessage(null)}>
41+
Close
42+
</Button>
43+
</DialogClose>
44+
</DialogFooter>
45+
</DialogContent>
46+
</Dialog>
47+
);
48+
};
49+
50+
export default TimetableErrorDialog;

0 commit comments

Comments
 (0)