Skip to content

Commit 8df8507

Browse files
fix: exclude meeting instance date from parent rrule
Excludes the original meeting instance date (before the meeting was updated) from the recurring parent meeting's rrule. Previously, TB was using the updated meeting time which would not properly exclude the meeting instance if it was moved forward into the future.
1 parent 240fb1b commit 8df8507

File tree

4 files changed

+45
-4
lines changed

4 files changed

+45
-4
lines changed

components/calendar/weekly-display.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,13 @@ function WeeklyDisplay({
7474
const [viewing, setViewing] = useState<Meeting>();
7575
const [now, setNow] = useState<Date>(new Date());
7676

77+
const originalEditing = useRef<Meeting>(initialEditData);
7778
const updateMeetingRemote = useCallback(async (updated: Meeting) => {
7879
const url = `/api/meetings/${updated.id}`;
79-
const { data } = await axios.put<MeetingJSON>(url, updated.toJSON());
80+
const { data } = await axios.put<MeetingJSON>(url, {
81+
...updated.toJSON(),
82+
options: { original: originalEditing.current.toJSON() },
83+
});
8084
return Meeting.fromJSON(data);
8185
}, []);
8286

@@ -94,6 +98,11 @@ function WeeklyDisplay({
9498
error: editError,
9599
} = useSingle<Meeting>(initialEditData, updateMeetingRemote, mutateMeeting);
96100

101+
useEffect(() => {
102+
if (editing.id !== originalEditing.current.id)
103+
originalEditing.current = editing;
104+
}, [editing]);
105+
97106
const headerRef = useRef<HTMLDivElement>(null);
98107
const timesRef = useRef<HTMLDivElement>(null);
99108
const rowsRef = useRef<HTMLDivElement>(null);

lib/api/routes/meetings/update.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import updateZoom from 'lib/api/update/zoom';
2020
import verifyAuth from 'lib/api/verify/auth';
2121
import verifyBody from 'lib/api/verify/body';
2222
import verifyDocExists from 'lib/api/verify/doc-exists';
23+
import verifyOptions from 'lib/api/verify/options';
24+
import verifyRecurIncludesTime from 'lib/api/verify/recur-includes-time';
2325
import verifySubjectsCanBeTutored from 'lib/api/verify/subjects-can-be-tutored';
2426
import verifyTimeInAvailability from 'lib/api/verify/time-in-availability';
2527

28+
export type UpdateMeetingOptions = { original: MeetingJSON };
2629
export type UpdateMeetingRes = MeetingJSON;
2730

2831
export default async function updateMeeting(
@@ -35,6 +38,9 @@ export default async function updateMeeting(
3538
isMeetingJSON,
3639
Meeting
3740
);
41+
const options = verifyOptions<UpdateMeetingOptions>(req.body, {
42+
original: body.toJSON(),
43+
});
3844

3945
const [matchDoc, meetingDoc] = await Promise.all([
4046
verifyDocExists('matches', body.match.id),
@@ -62,7 +68,7 @@ export default async function updateMeeting(
6268
// - Admins can change 'approved' to 'pending' or 'logged'.
6369
// - Meeting people can change 'pending' to 'logged'.
6470

65-
if (original.time.recur) {
71+
if (original.id !== body.id && original.time.recur) {
6672
// User is updating a recurring meeting. By default, we only update this
6773
// meeting and all future meetings:
6874
// 1. Create a new recurring meeting using this meeting's data.
@@ -72,16 +78,24 @@ export default async function updateMeeting(
7278
body.id = '';
7379
body.parentId = undefined;
7480
body.venue = await createZoom(body, people);
81+
82+
// Ensure that the `until` value isn't before the `time.from` value (which
83+
// would prevent *any* meeting instances from being calculated).
84+
body.time.recur = verifyRecurIncludesTime(body.time);
7585
body.time.last = getLastTime(body.time);
7686

7787
const meeting = await createMeetingDoc(body);
7888
await createMeetingSearchObj(meeting);
7989

80-
// TODO: We need to know the start time of the meeting instance before it
90+
// TODO: We need to know the start date of the meeting instance before it
8191
// was updated. Otherwise, we can't properly exclude it from the original.
8292
original.time.recur = RRule.optionsToString({
8393
...RRule.parseString(original.time.recur),
84-
until: body.time.from, // TODO: Replace with time before update.
94+
until: new Date(
95+
new Date(options.original.time.from).getFullYear(),
96+
new Date(options.original.time.from).getMonth(),
97+
new Date(options.original.time.from).getDate()
98+
),
8599
});
86100
original.time.last = getLastTime(original.time);
87101

lib/api/verify/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// TODO: Verify that the values at each key in `body.options` matches the
2+
// required data type in `T`. Only use values that are valid.
3+
export default function verifyOptions<T>(body: unknown, fallback: T): T {
4+
return { ...fallback, ...(body as { options: T }).options };
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { RRule } from 'rrule';
2+
3+
import { Timeslot } from 'lib/model';
4+
5+
// Ensure that the `until` value of an RRule isn't before the `time.from` value
6+
// (which would prevent *any* meeting instances from being calculated).
7+
export default function verifyRecurIncludesTime(time: Timeslot): string {
8+
const { until, ...options } = RRule.parseString(time.recur);
9+
if (!until || until >= time.to) return time.recur;
10+
// If the `until` value is before the timeslot end, we set it to the timeslot
11+
// end. Note that we could instead set it to the timeslot start (same effect).
12+
return RRule.optionsToString({ ...options, until: time.to });
13+
}

0 commit comments

Comments
 (0)