Skip to content

Commit 8dc6d14

Browse files
philrenaudpooja169uspozsivanovheatlikeheatwave
authored
Nylas Scheduler: Consecutive Booking (#226)
* Multiple events * Warning for divergent times * EventDefinition and host rules types * [Scheduler]: Consecutive availability initial set up * add startday to if condition * Deduping template * rather fetching * Methodifying store fetch and open hours conversions * Some type cleanup * Right up to the point where you can book * progress: hydrating subevents and deduping simultaneous * timeslots busy or free based on existince of block * Console cleanup * slot inspection on availability component * availability component as interface * Consecutive works with list view * relativizing formerly top-level props to event props * Default view back to calendar * Last date in availability query should be using end hour, not start hour * A little bit of cleanup and fixing empty dispatch event * Dirty-up but got events hydrating for single event situations * fix issue where consecutive event participants didnt match manifest event order * variablizing _this.events * General typefixing * Reset availability demo for tests * Couple more type fixes * email ids to participant within events and basic consecutive tests * unonly on tests * Intercepts added to tests * Some pre-review fixups * Fixup typings * Test for selecting event working with stubbed response * Fix type error * Removed TODO comment * Fixup to unblock qa on parsed email addresses * typing fixes * More type updates * participants to participantEmails * Demo allows you to switch between single and consec * Set() to filter out timedupes per @ozsivanov * De-debugZ * ID fix for availability Co-authored-by: Pooja Guggari <[email protected]> Co-authored-by: Oliver Zsivanov <[email protected]> Co-authored-by: Heather Hamilton <[email protected]>
1 parent d50c5cd commit 8dc6d14

File tree

22 files changed

+1032
-178
lines changed

22 files changed

+1032
-178
lines changed

commons/src/connections/availability.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import {
55
getMiddlewareApiUrl,
66
} from "../methods/api";
77
import type {
8+
ConsecutiveAvailabilityQuery,
89
AvailabilityQuery,
910
AvailabilityResponse,
1011
FreeBusyResponse,
11-
TimeSlot,
12+
PreDatedTimeSlot,
1213
} from "@commons/types/Availability";
1314
import type { MiddlewareResponse } from "@commons/types/Nylas";
15+
import type { EventDefinition } from "@commons/types/ScheduleEditor";
16+
import type { ConsecutiveEvent } from "@commonstypes/Scheduler";
1417

1518
// TODO: deprecate if we find /calendars/availability to be fully sufficient
1619
export const fetchFreeBusy = async (
@@ -63,3 +66,84 @@ export const fetchAvailability = async (
6366
})
6467
.catch((error) => handleError(query.component_id, error));
6568
};
69+
70+
export const fetchConsecutiveAvailability = async (
71+
query: ConsecutiveAvailabilityQuery,
72+
): Promise<ConsecutiveEvent[][]> => {
73+
return fetch(
74+
`${getMiddlewareApiUrl(
75+
query.component_id,
76+
)}/calendars/availability/consecutive`,
77+
getFetchConfig({
78+
method: "POST",
79+
component_id: query.component_id,
80+
access_token: query.access_token,
81+
body: query.body,
82+
}),
83+
)
84+
.then(async (apiResponse): Promise<ConsecutiveEvent[][]> => {
85+
const json = await handleResponse<
86+
MiddlewareResponse<PreDatedTimeSlot[][]>
87+
>(apiResponse);
88+
let response: PreDatedTimeSlot[][] =
89+
json.response?.map((blockSlot) => {
90+
blockSlot = blockSlot.map((slot: any) => {
91+
slot.start_time = new Date(slot.start_time * 1000);
92+
slot.end_time = new Date(slot.end_time * 1000);
93+
return slot;
94+
});
95+
return blockSlot;
96+
}) || [];
97+
const hydratedResponse = hydrateSlotsToEvents(
98+
response,
99+
query.body.events,
100+
);
101+
const dedupedResponse =
102+
removeSimultaneousAvailabilityOptions(hydratedResponse);
103+
return dedupedResponse;
104+
})
105+
.catch((error) => handleError(query.component_id, error));
106+
};
107+
108+
// Doing the best we can with what we've got: /calendars/availability/consecutive doesn't return any info other than emails
109+
// and start/end times. This means that if we have to events (EventDefinitions) with the same email addresses? We're shooting in the dark about which is which.
110+
// TODO: allow for an indicator on the API side
111+
function hydrateSlotsToEvents(
112+
availabilities: PreDatedTimeSlot[][],
113+
events: EventDefinition[],
114+
): ConsecutiveEvent[][] {
115+
return availabilities.map((block) => {
116+
return block.map((subevent) => {
117+
return {
118+
...subevent,
119+
...events.find(
120+
(eventdef) =>
121+
eventdef.participantEmails.length === subevent.emails.length &&
122+
eventdef.participantEmails.every((email) =>
123+
subevent.emails.includes(email),
124+
),
125+
),
126+
};
127+
});
128+
}) as any[][]; // TODO: How to best coerce PreDatedTimeSlot[][] to ConsecutiveEvent[][]? spread-combined return handles it.
129+
}
130+
131+
// We don't want to overburden our users with too much sweet horrible freedom of choice;
132+
// the /calendars/availability/consecutive endpoint returns order permutations with same time slots;
133+
// Cull them down to just the first that exists per timeslot.
134+
function removeSimultaneousAvailabilityOptions(
135+
availabilities: ConsecutiveEvent[][],
136+
) {
137+
const blockSet = new Set();
138+
return availabilities.filter((block) => {
139+
const blockString = `${block[0].start_time}_${
140+
block[block.length - 1].end_time
141+
}`;
142+
if (blockSet.has(blockString)) {
143+
return false;
144+
} else {
145+
blockSet.add(blockString);
146+
return true;
147+
}
148+
});
149+
}

commons/src/connections/manifest.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,22 @@ export const fetchManifest = async (
2424

2525
// Allows <nylas-schedule-editor> to modify its own properties
2626

27-
interface saveManifestParams {
28-
id: string;
29-
access_token?: string;
30-
manifest: Manifest;
31-
}
32-
3327
export const saveManifest = async (
34-
params: saveManifestParams
28+
id: string,
29+
manifest: Manifest,
30+
access_token?: string,
3531
): Promise<Manifest> => {
36-
const { id, access_token, manifest } = params;
3732
return fetch(
3833
`${getMiddlewareApiUrl(id)}/component`,
3934
getFetchConfig({
4035
method: "PUT",
4136
component_id: id,
4237
access_token,
43-
body: manifest,
38+
body: { settings: manifest },
4439
}),
4540
)
4641
.then((response) => handleResponse<MiddlewareResponse<Manifest>>(response))
4742
.then((json) => {
4843
return json.response;
4944
});
50-
}
45+
};

commons/src/constants/custom-fields.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const DefaultCustomFields = [
1+
import type { CustomField } from "@commons/types/Scheduler";
2+
3+
export const DefaultCustomFields: CustomField[] = [
24
{
35
title: "Your Name",
46
type: "text",

commons/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
} from "./connections/neural";
2727

2828
export { AvailabilityStore } from "./store/availability";
29+
export { ConsecutiveAvailabilityStore } from "./store/consecutive-availability";
2930
export { CalendarStore } from "./store/calendars";
3031
export { ContactStore } from "./store/contacts";
3132
export { ContactAvatarStore } from "./store/contact-avatar";

commons/src/methods/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function parseBoolean(
102102
return (<any>[true, "true", "1"]).includes(val);
103103
}
104104

105-
export default function parseStringToArray(parseStr: string) {
105+
export default function parseStringToArray(parseStr: string): string[] {
106106
if (!parseStr) {
107107
return [];
108108
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Writable, writable } from "svelte/store";
2+
import { fetchConsecutiveAvailability } from "../connections/availability";
3+
import type {
4+
AvailabilityQuery,
5+
ConsecutiveAvailabilityQuery,
6+
TimeSlot,
7+
} from "@commons/types/Availability";
8+
import type { ConsecutiveEvent } from "@commonstypes/Scheduler";
9+
10+
type ConsecutiveAvailabilityStore = Record<
11+
string,
12+
Promise<ConsecutiveEvent[][]>
13+
>;
14+
15+
function initialize(): Writable<ConsecutiveAvailabilityStore> {
16+
const get = (
17+
target: ConsecutiveAvailabilityStore,
18+
key: string,
19+
): Promise<ConsecutiveEvent[][]> | void => {
20+
const accessor: ConsecutiveAvailabilityQuery = JSON.parse(key);
21+
// Avoid saving forceReload property as part of store key
22+
const accessorCopy = { ...accessor };
23+
delete accessorCopy.forceReload;
24+
key = JSON.stringify(accessorCopy);
25+
26+
if (
27+
!accessor.component_id ||
28+
!accessor?.body?.start_time ||
29+
!accessor?.body?.end_time
30+
) {
31+
return;
32+
}
33+
34+
if (!target[key] || accessor.forceReload) {
35+
const fetchPromise = fetchConsecutiveAvailability(accessor);
36+
store.update((store) => {
37+
store[key] = fetchPromise;
38+
return store;
39+
});
40+
target[key] = fetchPromise;
41+
}
42+
return target[key];
43+
};
44+
const store = writable(new Proxy<ConsecutiveAvailabilityStore>({}, { get }));
45+
return store;
46+
}
47+
48+
export const ConsecutiveAvailabilityStore = initialize();

commons/src/types/Availability.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import type {
66
CommonQuery,
77
Manifest as NylasManifest,
88
} from "@commons/types/Nylas";
9+
10+
import type { EventDefinition } from "./ScheduleEditor";
11+
912
export interface Manifest extends NylasManifest {
1013
allow_booking: boolean;
1114
allow_date_change: boolean;
@@ -18,7 +21,6 @@ export interface Manifest extends NylasManifest {
1821
closed_color: string;
1922
date_format: "full" | "weekday" | "date" | "none";
2023
dates_to_show: number;
21-
email_ids: string[];
2224
participants: string[];
2325
end_hour: number;
2426
event_buffer: number;
@@ -43,6 +45,7 @@ export interface Manifest extends NylasManifest {
4345
start_hour: number;
4446
view_as: "schedule" | "list";
4547
timezone: string;
48+
events: EventDefinition[];
4649
}
4750

4851
export interface AvailabilityRule {
@@ -73,9 +76,25 @@ export interface TimeSlot {
7376
end_time: Date;
7477
available_calendars: string[];
7578
calendar_id?: string;
79+
expirySelection?: string;
80+
recurrence_cadence?: string;
81+
recurrence_expiry?: Date | string;
82+
}
83+
84+
export interface BookableSlot extends TimeSlot {
85+
event_conferencing: string;
86+
event_description: string;
87+
event_location: string;
88+
event_title: string;
7689
expirySelection: string;
77-
recurrence_cadence: string;
78-
recurrence_expiry: string;
90+
recurrence_cadence?:
91+
| "none"
92+
| "daily"
93+
| "weekdays"
94+
| "biweekly"
95+
| "weekly"
96+
| "monthly";
97+
recurrence_expiry?: Date | string;
7998
}
8099

81100
export interface SelectableSlot extends TimeSlot {
@@ -94,6 +113,21 @@ export interface AvailabilityQuery extends CommonQuery {
94113
free_busy: any[];
95114
duration_minutes: number;
96115
interval_minutes: number;
116+
round_robin?: string;
117+
};
118+
forceReload?: boolean;
119+
}
120+
121+
export interface ConsecutiveAvailabilityQuery extends CommonQuery {
122+
body: {
123+
emails: string[];
124+
start_time: number;
125+
end_time: number;
126+
free_busy: any[];
127+
duration_minutes: number;
128+
interval_minutes: number;
129+
events: EventDefinition[];
130+
round_robin: "max-availability" | "max-fairness";
97131
};
98132
forceReload?: boolean;
99133
}
@@ -138,3 +172,12 @@ export interface Day {
138172
slots: SelectableSlot[];
139173
timestamp: Date;
140174
}
175+
176+
export interface OpenHours {
177+
emails: string[];
178+
days: number[];
179+
start: string;
180+
end: string;
181+
timezone: string;
182+
object_type: "open_hours";
183+
}

commons/src/types/Events.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,26 @@ import type { Participant } from "@commons/types/Nylas";
22
import type { EventStatus } from "@commons/enums/Events";
33

44
interface _Event {
5-
title?: string;
6-
description?: string;
7-
participants?: Participant[];
8-
owner: string;
9-
id: string;
10-
calendar_id: string;
115
account_id: string;
126
busy: boolean;
13-
status?: EventStatus;
14-
relativeStartTime: number;
15-
relativeRunTime: number;
7+
calendar_id: string;
8+
id: string;
9+
owner: string;
10+
recurrence: EventRecurrence;
1611
relativeOverlapOffset: number;
1712
relativeOverlapWidth: number;
18-
location?: string;
19-
locationString?: string;
13+
relativeRunTime: number;
14+
relativeStartTime: number;
2015
attendeeStatus?: "yes" | "no" | "noreply" | "maybe";
21-
isNewEvent?: boolean;
2216
conferencing?: EventConferencing;
23-
recurrence: EventRecurrence;
17+
description?: string;
18+
isNewEvent?: boolean;
19+
location?: string;
20+
locationString?: string;
21+
metadata?: Record<string, any>;
22+
participants?: Participant[];
23+
status?: EventStatus;
24+
title?: string;
2425
}
2526

2627
export interface EventRecurrence {

commons/src/types/Nylas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export interface MiddlewareResponse<T = unknown> {
145145

146146
export interface NError {
147147
name?: string;
148-
message?: Error;
148+
message?: Error | string;
149149
}
150150

151151
export interface Manifest {

commons/src/types/ScheduleEditor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export interface Manifest extends NylasManifest {
4141
max_book_ahead_days: number;
4242
min_book_ahead_days: number;
4343
custom_fields: CustomField[];
44-
events: any[]; // TODO
44+
events: EventDefinition[];
4545
}
4646

4747
export interface EventDefinition {
@@ -50,7 +50,7 @@ export interface EventDefinition {
5050
event_location: string;
5151
event_conferencing: string;
5252
slot_size: 15 | 30 | 60;
53-
email_ids: string[];
53+
participantEmails: string[];
5454
host_rules: HostRules;
5555
}
5656

0 commit comments

Comments
 (0)