Skip to content

Commit 21d01bb

Browse files
feat(model): add full recur rule support
1 parent aef41e8 commit 21d01bb

File tree

3 files changed

+55
-52
lines changed

3 files changed

+55
-52
lines changed

lib/model/availability.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ import {
1010
import { getDate, getMonthsTimeslots, sameDate } from 'lib/utils/time';
1111
import clone from 'lib/utils/clone';
1212

13-
/**
14-
* One's schedule contains all your booked timeslots (the inverse of one's
15-
* availability).
16-
* @deprecated We have no use of this for now (though we might in the future
17-
* when we implement a dashboard view).
18-
*/
19-
export type ScheduleAlias = TimeslotInterface[];
20-
2113
/**
2214
* One's availability contains all your open timeslots (the inverse of one's
2315
* schedule).
@@ -63,9 +55,7 @@ export class Availability extends Array<Timeslot> implements AvailabilityAlias {
6355
* avail.sort(); // Returns [past, now, future] sort.
6456
*/
6557
public sort(): this {
66-
return super.sort((timeslotA, timeslotB) => {
67-
return timeslotA.from.valueOf() - timeslotB.from.valueOf();
68-
});
58+
return super.sort((a, b) => a.from.valueOf() - b.from.valueOf());
6959
}
7060

7161
public static full(month: number, year: number): Availability {
@@ -101,15 +91,6 @@ export class Availability extends Array<Timeslot> implements AvailabilityAlias {
10191
return new Availability(...this.filter((t) => timeslotOnDate(t, date)));
10292
}
10393

104-
/**
105-
* @return Whether or not this availability contains the exact given timeslot.
106-
* @deprecated I'm not sure where I would need to use this, but whereever I do
107-
* it should be removed.
108-
*/
109-
public hasTimeslot(timeslot: TimeslotInterface): boolean {
110-
return this.some((t) => t.equalTo(timeslot));
111-
}
112-
11394
/**
11495
* @return Whether this availability overlaps at all with the given timeslot.
11596
*/
@@ -191,19 +172,6 @@ export class Availability extends Array<Timeslot> implements AvailabilityAlias {
191172
updated.forEach((time: Timeslot) => this.push(time));
192173
}
193174

194-
/**
195-
* Returns whether two availabilities contain all the same timeslots.
196-
* @param other - The other availability to check against.
197-
* @return Whether this availability contained all the same timeslots as the
198-
* other availability.
199-
* @deprecated We should just use a `dequal` which should do the same thing.
200-
*/
201-
public equalTo(other: Availability): boolean {
202-
if (!other.every((t: Timeslot) => this.hasTimeslot(t))) return false;
203-
if (!this.every((t: Timeslot) => other.hasTimeslot(t))) return false;
204-
return true;
205-
}
206-
207175
public toString(locale = 'en'): string {
208176
return this.map((t) => t.toString(locale)).join(', ');
209177
}

lib/model/meeting.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type MeetingStatus = 'created' | 'pending' | 'logged' | 'approved';
6161
* @property time - Time of the meeting (e.g. Tuesday 3:00 - 3:30 PM).
6262
* @property creator - The person who logged the meeting (typically the tutor).
6363
* @property notes - Notes about the meeting (e.g. what they worked on).
64+
* @property [parentId] - The recurring parent meeting ID (if any).
6465
*/
6566
export interface MeetingInterface extends ResourceInterface {
6667
status: MeetingStatus;
@@ -70,6 +71,7 @@ export interface MeetingInterface extends ResourceInterface {
7071
time: Timeslot;
7172
notes: string;
7273
ref?: DocumentReference;
74+
parentId?: string;
7375
id: string;
7476
}
7577

@@ -106,6 +108,7 @@ export function isMeetingJSON(json: unknown): json is MeetingJSON {
106108
if (!isVenueJSON(json.venue)) return false;
107109
if (!isTimeslotJSON(json.time)) return false;
108110
if (typeof json.notes !== 'string') return false;
111+
if (json.parentId && typeof json.parentId !== 'string') return false;
109112
if (typeof json.id !== 'string') return false;
110113
return true;
111114
}
@@ -129,6 +132,8 @@ export class Meeting extends Resource implements MeetingInterface {
129132

130133
public notes = '';
131134

135+
public parentId?: string;
136+
132137
public id = '';
133138

134139
public ref?: DocumentReference;

lib/model/timeslot.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as admin from 'firebase-admin';
33
import { isDateJSON, isJSON } from 'lib/model/json';
44
import clone from 'lib/utils/clone';
55
import construct from 'lib/model/construct';
6+
import definedVals from 'lib/model/defined-vals';
67

78
/**
89
* This is a painful workaround as we then import the entire Firebase library
@@ -18,31 +19,36 @@ type Timestamp = admin.firestore.Timestamp;
1819

1920
/**
2021
* A timeslot is a window of time and provides all the necessary scheduling data
21-
* for any scenario.
22+
* for any scenario (including support for complex rrules used server-side).
23+
* @typedef {Object} TimeslotInterface
2224
* @property id - A unique identifier for this timeslot (used as React keys and
2325
* thus only stored client-side as we have no use for this on our server).
24-
* @property from - The start time and date of this timeslot (typically
25-
* represented by a `Date`, `Timestamp`, or UTC date string).
26-
* @property to - The end time and date of this timeslot (represented in the
27-
* same format as the `from` property).
26+
* @property from - The start time of this particular timeslot instance.
27+
* @property to - The end time of this particular timeslot instance.
28+
* @property recur - The timeslot's recurrence rule (uses the iCal RFC string).
29+
* @property [last] - The timeslot's last possible end time. Undefined
30+
* client-side; only used server-side for querying recurring timeslots.
2831
*/
29-
export interface TimeslotBase<T> {
32+
export interface TimeslotInterface<T = Date> {
3033
id: string;
3134
from: T;
3235
to: T;
36+
recur: string;
37+
last?: T;
3338
}
3439

35-
export type TimeslotInterface = TimeslotBase<Date>;
36-
export type TimeslotFirestore = TimeslotBase<Timestamp>;
37-
export type TimeslotJSON = TimeslotBase<string>;
38-
export type TimeslotSearchHit = TimeslotBase<number>;
40+
export type TimeslotFirestore = TimeslotInterface<Timestamp>;
41+
export type TimeslotJSON = TimeslotInterface<string>;
42+
export type TimeslotSearchHit = TimeslotInterface<number>;
3943
export type TimeslotSegment = { from: Date; to: Date };
4044

4145
export function isTimeslotJSON(json: unknown): json is TimeslotJSON {
4246
if (!isJSON(json)) return false;
47+
if (typeof json.id !== 'string') return false;
4348
if (!isDateJSON(json.from)) return false;
4449
if (!isDateJSON(json.to)) return false;
45-
if (typeof json.id !== 'string') return false;
50+
if (typeof json.recur !== 'string') return false;
51+
if (json.last && !isDateJSON(json.last)) return false;
4652
return true;
4753
}
4854

@@ -53,6 +59,11 @@ export class Timeslot implements TimeslotInterface {
5359

5460
public to: Date = new Date();
5561

62+
// TODO: Should I prefill this with `RRULE:COUNT=1` for single instances?
63+
public recur = '';
64+
65+
public last?: Date;
66+
5667
/**
5768
* Constructor that takes advantage of Typescript's shorthand assignment.
5869
* @see {@link https://bit.ly/2XjNmB5}
@@ -77,6 +88,7 @@ export class Timeslot implements TimeslotInterface {
7788
* 1. (Contained) Timeslot contains the given timeslot, OR;
7889
* 2. (Overlap Start) Timeslot contains the given timeslot's start time, OR;
7990
* 3. (Overlap End) Timeslot contains the given timeslot's end time.
91+
* @todo Why can't we use this in the calendar positioning logic?
8092
*/
8193
public overlaps(other: { from: Date; to: Date }): boolean {
8294
return (
@@ -143,45 +155,59 @@ export class Timeslot implements TimeslotInterface {
143155
}
144156

145157
public toFirestore(): TimeslotFirestore {
146-
const { from, to, ...rest } = this;
147-
return {
158+
const { from, to, last, ...rest } = this;
159+
return definedVals({
148160
...rest,
149161
from: (from as unknown) as Timestamp,
150162
to: (to as unknown) as Timestamp,
151-
};
163+
last: last ? ((last as unknown) as Timestamp) : undefined,
164+
});
152165
}
153166

154167
public static fromFirestore(data: TimeslotFirestore): Timeslot {
155168
return new Timeslot({
156169
...data,
157170
from: data.from.toDate(),
158171
to: data.to.toDate(),
172+
last: data.last?.toDate(),
159173
});
160174
}
161175

162176
public toJSON(): TimeslotJSON {
163-
const { from, to, ...rest } = this;
164-
return { ...rest, from: from.toJSON(), to: to.toJSON() };
177+
const { from, to, last, ...rest } = this;
178+
return definedVals({
179+
...rest,
180+
from: from.toJSON(),
181+
to: to.toJSON(),
182+
last: last?.toJSON(),
183+
});
165184
}
166185

167186
public static fromJSON(json: TimeslotJSON): Timeslot {
168187
return new Timeslot({
169188
...json,
170189
from: new Date(json.from),
171190
to: new Date(json.to),
191+
last: json.last ? new Date(json.last) : undefined,
172192
});
173193
}
174194

175195
public toSearchHit(): TimeslotSearchHit {
176-
const { from, to, ...rest } = this;
177-
return { ...rest, from: from.valueOf(), to: to.valueOf() };
196+
const { from, to, last, ...rest } = this;
197+
return definedVals({
198+
...rest,
199+
from: from.valueOf(),
200+
to: to.valueOf(),
201+
last: last?.valueOf(),
202+
});
178203
}
179204

180205
public static fromSearchHit(hit: TimeslotSearchHit): Timeslot {
181206
return new Timeslot({
182207
...hit,
183208
from: new Date(hit.from),
184209
to: new Date(hit.to),
210+
last: hit.last ? new Date(hit.last) : undefined,
185211
});
186212
}
187213

@@ -192,9 +218,13 @@ export class Timeslot implements TimeslotInterface {
192218
public static fromURLParam(param: string): Timeslot {
193219
const params: URLSearchParams = new URLSearchParams(param);
194220
return new Timeslot({
221+
id: params.get('id') || undefined,
195222
from: new Date(params.get('from') as string),
196223
to: new Date(params.get('to') as string),
197-
id: params.get('id') || undefined,
224+
recur: params.get('recur') || undefined,
225+
last: params.get('last')
226+
? new Date(params.get('last') as string)
227+
: undefined,
198228
});
199229
}
200230

0 commit comments

Comments
 (0)