Daily video calls (#542)

* ⬆️ Bump tailwindcss from 2.2.14 to 2.2.15

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.14 to 2.2.15.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* daily table

* rebasing updates

* updating Daily references to a new table

* updating internal notes

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* changing calendso video powered by Daily.co to cal video powered by Daily.co

* updating some of the internal Daily notes

* added a modal for when the call/ link is invalid

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* fixing spacing on the cancel booking modal and adding Daily references in the event manager

* formatting fixes

* updating the readme file

* adding a daily interface in the event manager

* adding daily to the location labels

* added a note to cal event parser

* resolving yarn merge conflicts

* updating dailyReturn to DailyReturnType

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updating read me formatting

* indent space for Daily ReadMe section

* resolving the merge conflicts in the yarn file

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lola-Ojabowale <lola.ojabowale@gmail.com>
This commit is contained in:
Lola 2021-10-07 12:12:39 -04:00 committed by GitHub
parent 58de920951
commit adee3fd211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 761 additions and 41 deletions

View file

@ -31,6 +31,9 @@ MS_GRAPH_CLIENT_SECRET=
ZOOM_CLIENT_ID= ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET= ZOOM_CLIENT_SECRET=
#Used for the Daily integration
DAILY_API_KEY=
# E-mail settings # E-mail settings
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to # Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View file

@ -283,6 +283,13 @@ Contributions are what make the open source community such an amazing place to b
12. Click "Done". 12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings. 13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
## Obtaining Daily API Credentials
1. Open [Daily](https://www.daily.co/) and sign into your account.
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
<!-- LICENSE --> <!-- LICENSE -->
## License ## License

View file

@ -71,6 +71,7 @@ const BookingPage = (props: BookingPageProps) => {
[LocationType.Phone]: "Phone call", [LocationType.Phone]: "Phone call",
[LocationType.GoogleMeet]: "Google Meet", [LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video", [LocationType.Zoom]: "Zoom Video",
[LocationType.Daily]: "Daily.co Video",
}; };
const _bookingHandler = (event) => { const _bookingHandler = (event) => {

View file

@ -80,13 +80,17 @@ export default class CalEventParser {
/** /**
* Conditionally returns the event's location. When VideoCallData is set, * Conditionally returns the event's location. When VideoCallData is set,
* it returns the meeting url. Otherwise, the regular location is returned. * it returns the meeting url. Otherwise, the regular location is returned.
* * For Daily video calls returns the direct link
* @protected * @protected
*/ */
protected getLocation(): string | undefined { protected getLocation(): string | undefined {
const isDaily = this.calEvent.location === "integrations:daily";
if (this.optionalVideoCallData) { if (this.optionalVideoCallData) {
return this.optionalVideoCallData.url; return this.optionalVideoCallData.url;
} }
if (isDaily) {
return process.env.BASE_URL + "/call/" + this.getUid();
}
return this.calEvent.location; return this.calEvent.location;
} }

239
lib/dailyVideoClient.ts Normal file
View file

@ -0,0 +1,239 @@
import { Credential } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import CalEventParser from "@lib/CalEventParser";
import { AdditionInformation, EntryPoint } from "@lib/emails/EventMail";
import { getIntegrationName } from "@lib/emails/helpers";
import { EventResult } from "@lib/events/EventManager";
import logger from "@lib/logger";
import { CalendarEvent } from "./calendarClient";
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail";
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
const log = logger.getChildLogger({ prefix: ["[lib] dailyVideoClient"] });
const translator = short();
export interface DailyVideoCallData {
type: string;
id: string;
password: string;
url: string;
}
function handleErrorsJson(response) {
if (!response.ok) {
response.json().then(console.log);
throw Error(response.statusText);
}
return response.json();
}
const dailyCredential = process.env.DAILY_API_KEY;
interface DailyVideoApiAdapter {
dailyCreateMeeting(event: CalendarEvent): Promise<any>;
dailyUpdateMeeting(uid: string, event: CalendarEvent);
dailyDeleteMeeting(uid: string): Promise<unknown>;
getAvailability(dateFrom, dateTo): Promise<any>;
}
const DailyVideo = (credential): DailyVideoApiAdapter => {
const translateEvent = (event: CalendarEvent) => {
// Documentation at: https://docs.daily.co/reference#list-rooms
// added a 1 hour buffer for room expiration and room entry
const exp = Math.round(new Date(event.endTime).getTime() / 1000) + 60 * 60;
const nbf = Math.round(new Date(event.startTime).getTime() / 1000) - 60 * 60;
return {
privacy: "private",
properties: {
enable_new_call_ui: true,
enable_prejoin_ui: true,
enable_knocking: true,
enable_screenshare: true,
enable_chat: true,
exp: exp,
nbf: nbf,
},
};
};
return {
getAvailability: () => {
return credential;
},
dailyCreateMeeting: (event: CalendarEvent) =>
fetch("https://api.daily.co/v1/rooms", {
method: "POST",
headers: {
Authorization: "Bearer " + dailyCredential,
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
}).then(handleErrorsJson),
dailyDeleteMeeting: (uid: string) =>
fetch("https://api.daily.co/v1/rooms/" + uid, {
method: "DELETE",
headers: {
Authorization: "Bearer " + dailyCredential,
},
}).then(handleErrorsJson),
dailyUpdateMeeting: (uid: string, event: CalendarEvent) =>
fetch("https://api.daily.co/v1/rooms/" + uid, {
method: "POST",
headers: {
Authorization: "Bearer " + dailyCredential,
"Content-Type": "application/json",
},
body: JSON.stringify(translateEvent(event)),
}).then(handleErrorsJson),
};
};
// factory
const videoIntegrations = (withCredentials): DailyVideoApiAdapter[] =>
withCredentials
.map((cred) => {
return DailyVideo(cred);
})
.filter(Boolean);
const getBusyVideoTimes: (withCredentials) => Promise<unknown[]> = (withCredentials) =>
Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) =>
results.reduce((acc, availability) => acc.concat(availability), [])
);
const dailyCreateMeeting = async (
credential: Credential,
calEvent: CalendarEvent,
maybeUid: string = null
): Promise<EventResult> => {
const parser: CalEventParser = new CalEventParser(calEvent, maybeUid);
const uid: string = parser.getUid();
if (!credential) {
throw new Error(
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
);
}
let success = true;
const creationResult = await videoIntegrations([credential])[0]
.dailyCreateMeeting(calEvent)
.catch((e) => {
log.error("createMeeting failed", e, calEvent);
success = false;
});
const currentRoute = process.env.BASE_URL;
const videoCallData: DailyVideoCallData = {
type: "Daily.co Video",
id: creationResult.name,
password: creationResult.password,
url: currentRoute + "/call/" + uid,
};
const entryPoint: EntryPoint = {
entryPointType: getIntegrationName(videoCallData),
uri: videoCallData.url,
label: "Enter Meeting",
pin: "",
};
const additionInformation: AdditionInformation = {
entryPoints: [entryPoint],
};
const organizerMail = new VideoEventOrganizerMail(calEvent, uid, videoCallData, additionInformation);
const attendeeMail = new VideoEventAttendeeMail(calEvent, uid, videoCallData, additionInformation);
try {
await organizerMail.sendEmail();
} catch (e) {
console.error("organizerMail.sendEmail failed", e);
}
if (!creationResult || !creationResult.disableConfirmationEmail) {
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
return {
type: "daily",
success,
uid,
createdEvent: creationResult,
originalEvent: calEvent,
};
};
const dailyUpdateMeeting = async (
credential: Credential,
uidToUpdate: string,
calEvent: CalendarEvent
): Promise<EventResult> => {
const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
if (!credential) {
throw new Error(
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
);
}
let success = true;
const updateResult = credential
? await videoIntegrations([credential])[0]
.dailyUpdateMeeting(uidToUpdate, calEvent)
.catch((e) => {
log.error("updateMeeting failed", e, calEvent);
success = false;
})
: null;
const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid);
const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid);
try {
await organizerMail.sendEmail();
} catch (e) {
console.error("organizerMail.sendEmail failed", e);
}
if (!updateResult || !updateResult.disableConfirmationEmail) {
try {
await attendeeMail.sendEmail();
} catch (e) {
console.error("attendeeMail.sendEmail failed", e);
}
}
return {
type: credential.type,
success,
uid: newUid,
updatedEvent: updateResult,
originalEvent: calEvent,
};
};
const dailyDeleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
if (credential) {
return videoIntegrations([credential])[0].dailyDeleteMeeting(uid);
}
return Promise.resolve({});
};
export { getBusyVideoTimes, dailyCreateMeeting, dailyUpdateMeeting, dailyDeleteMeeting };

View file

@ -25,6 +25,10 @@ export default class VideoEventAttendeeMail extends EventAttendeeMail {
* @protected * @protected
*/ */
protected getAdditionalBody(): string { protected getAdditionalBody(): string {
const meetingPassword = this.videoCallData.password;
const meetingId = getFormattedMeetingId(this.videoCallData);
if (meetingId && meetingPassword) {
return ` return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br /> <strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br /> <strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br />
@ -32,4 +36,10 @@ export default class VideoEventAttendeeMail extends EventAttendeeMail {
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br /> <strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`; `;
} }
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}
} }

View file

@ -26,11 +26,19 @@ export default class VideoEventOrganizerMail extends EventOrganizerMail {
* @protected * @protected
*/ */
protected getAdditionalBody(): string { protected getAdditionalBody(): string {
const meetingPassword = this.videoCallData.password;
const meetingId = getFormattedMeetingId(this.videoCallData);
// This odd indentation is necessary because otherwise the leading tabs will be applied into the event description. // This odd indentation is necessary because otherwise the leading tabs will be applied into the event description.
if (meetingPassword && meetingId) {
return ` return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br /> <strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br /> <strong>Meeting ID:</strong> ${getFormattedMeetingId(this.videoCallData)}<br />
<strong>Meeting Password:</strong> ${this.videoCallData.password}<br /> <strong>Meeting Password:</strong> ${this.videoCallData.password}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`;
}
return `
<strong>Video call provider:</strong> ${getIntegrationName(this.videoCallData)}<br />
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br /> <strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
`; `;
} }

View file

@ -4,6 +4,7 @@ import merge from "lodash.merge";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient"; import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
import { dailyCreateMeeting, dailyUpdateMeeting } from "@lib/dailyVideoClient";
import EventAttendeeMail from "@lib/emails/EventAttendeeMail"; import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail"; import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
import { LocationType } from "@lib/location"; import { LocationType } from "@lib/location";
@ -43,6 +44,9 @@ interface GetLocationRequestFromIntegrationRequest {
location: string; location: string;
} }
//const to idenfity a daily event location
const dailyLocation = "integrations:daily";
export default class EventManager { export default class EventManager {
calendarCredentials: Array<Credential>; calendarCredentials: Array<Credential>;
videoCredentials: Array<Credential>; videoCredentials: Array<Credential>;
@ -55,6 +59,19 @@ export default class EventManager {
constructor(credentials: Array<Credential>) { constructor(credentials: Array<Credential>) {
this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar")); this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar"));
this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
//for Daily.co video, temporarily pushes a credential for the daily-video-client
const hasDailyIntegration = process.env.DAILY_API_KEY;
const dailyCredential: Credential = {
id: +new Date().getTime(),
type: "daily_video",
key: { apikey: process.env.DAILY_API_KEY },
userId: +new Date().getTime(),
};
if (hasDailyIntegration) {
this.videoCredentials.push(dailyCredential);
}
} }
/** /**
@ -91,6 +108,17 @@ export default class EventManager {
); );
const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => { const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
const isDailyResult = result.type === "daily";
if (isDailyResult) {
return {
type: result.type,
uid: result.createdEvent.name.toString(),
meetingId: result.videoCallData?.id.toString(),
meetingPassword: result.videoCallData?.password,
meetingUrl: result.videoCallData?.url,
};
}
if (!isDailyResult) {
return { return {
type: result.type, type: result.type,
uid: result.createdEvent.id.toString(), uid: result.createdEvent.id.toString(),
@ -98,6 +126,7 @@ export default class EventManager {
meetingPassword: result.videoCallData?.password, meetingPassword: result.videoCallData?.password,
meetingUrl: result.videoCallData?.url, meetingUrl: result.videoCallData?.url,
}; };
}
}); });
return { return {
@ -136,7 +165,8 @@ export default class EventManager {
}, },
}); });
const isDedicated = EventManager.isDedicatedIntegration(event.location); const isDedicated =
EventManager.isDedicatedIntegration(event.location) || event.location === dailyLocation;
let results: Array<EventResult> = []; let results: Array<EventResult> = [];
let optionalVideoCallData: VideoCallData | undefined = undefined; let optionalVideoCallData: VideoCallData | undefined = undefined;
@ -198,6 +228,7 @@ export default class EventManager {
* @param optionalVideoCallData * @param optionalVideoCallData
* @private * @private
*/ */
private createAllCalendarEvents( private createAllCalendarEvents(
event: CalendarEvent, event: CalendarEvent,
noMail: boolean, noMail: boolean,
@ -215,8 +246,10 @@ export default class EventManager {
* @param event * @param event
* @private * @private
*/ */
private getVideoCredential(event: CalendarEvent): Credential | undefined { private getVideoCredential(event: CalendarEvent): Credential | undefined {
const integrationName = event.location.replace("integrations:", ""); const integrationName = event.location.replace("integrations:", "");
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName)); return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
} }
@ -232,8 +265,12 @@ export default class EventManager {
private createVideoEvent(event: CalendarEvent, maybeUid?: string): Promise<EventResult> { private createVideoEvent(event: CalendarEvent, maybeUid?: string): Promise<EventResult> {
const credential = this.getVideoCredential(event); const credential = this.getVideoCredential(event);
if (credential) { const isDaily = event.location === dailyLocation;
if (credential && !isDaily) {
return createMeeting(credential, event, maybeUid); return createMeeting(credential, event, maybeUid);
} else if (isDaily) {
return dailyCreateMeeting(credential, event, maybeUid);
} else { } else {
return Promise.reject("No suitable credentials given for the requested integration name."); return Promise.reject("No suitable credentials given for the requested integration name.");
} }
@ -271,8 +308,9 @@ export default class EventManager {
*/ */
private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) { private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
const credential = this.getVideoCredential(event); const credential = this.getVideoCredential(event);
const isDaily = event.location === dailyLocation;
if (credential) { if (credential && !isDaily) {
const bookingRef = booking.references.filter((ref) => ref.type === credential.type)[0]; const bookingRef = booking.references.filter((ref) => ref.type === credential.type)[0];
return updateMeeting(credential, bookingRef.uid, event).then((returnVal: EventResult) => { return updateMeeting(credential, bookingRef.uid, event).then((returnVal: EventResult) => {
@ -283,6 +321,10 @@ export default class EventManager {
return returnVal; return returnVal;
}); });
} else { } else {
if (isDaily) {
const bookingRefUid = booking.references.filter((ref) => ref.type === "daily")[0].uid;
return dailyUpdateMeeting(credential, bookingRefUid, event);
}
return Promise.reject("No suitable credentials given for the requested integration name."); return Promise.reject("No suitable credentials given for the requested integration name.");
} }
} }
@ -300,7 +342,8 @@ export default class EventManager {
*/ */
private static isDedicatedIntegration(location: string): boolean { private static isDedicatedIntegration(location: string): boolean {
// Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't. // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
return location === "integrations:zoom";
return location === "integrations:zoom" || location === dailyLocation;
} }
/** /**
@ -313,7 +356,11 @@ export default class EventManager {
private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) { private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) {
const location = locationObj.location; const location = locationObj.location;
if (location === LocationType.GoogleMeet.valueOf() || location === LocationType.Zoom.valueOf()) { if (
location === LocationType.GoogleMeet.valueOf() ||
location === LocationType.Zoom.valueOf() ||
location === LocationType.Daily.valueOf()
) {
const requestId = uuidv5(location, uuidv5.URL); const requestId = uuidv5(location, uuidv5.URL);
return { return {

View file

@ -12,8 +12,8 @@ export function getIntegrationName(name: string) {
return "Stripe"; return "Stripe";
case "apple_calendar": case "apple_calendar":
return "Apple Calendar"; return "Apple Calendar";
default: case "daily_video":
return "Unknown"; return "Daily";
} }
} }

View file

@ -3,4 +3,5 @@ export enum LocationType {
Phone = "phone", Phone = "phone",
GoogleMeet = "integrations:google:meet", GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom", Zoom = "integrations:zoom",
Daily = "integrations:daily",
} }

View file

@ -28,6 +28,7 @@
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.1", "@headlessui/react": "^1.4.1",
"@daily-co/daily-js": "^0.16.0",
"@heroicons/react": "^1.0.4", "@heroicons/react": "^1.0.4",
"@hookform/resolvers": "^2.8.1", "@hookform/resolvers": "^2.8.1",
"@jitsu/sdk-js": "^2.2.4", "@jitsu/sdk-js": "^2.2.4",
@ -84,6 +85,7 @@
"react-select": "^4.3.1", "react-select": "^4.3.1",
"react-timezone-select": "^1.0.7", "react-timezone-select": "^1.0.7",
"react-use-intercom": "1.4.0", "react-use-intercom": "1.4.0",
"react-router-dom": "^5.2.0",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"stripe": "^8.168.0", "stripe": "^8.168.0",
"superjson": "1.7.5", "superjson": "1.7.5",

View file

@ -22,6 +22,13 @@ import { getBusyVideoTimes } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload"; import sendPayload from "@lib/webhooks/sendPayload";
import getSubscriberUrls from "@lib/webhooks/subscriberUrls"; import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
export interface DailyReturnType {
name: string;
url: string;
id: string;
created_at: string;
}
dayjs.extend(dayjsBusinessDays); dayjs.extend(dayjsBusinessDays);
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(isBetween); dayjs.extend(isBetween);
@ -249,7 +256,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const attendeesList = [...invitee, ...guests, ...teamMembers]; const attendeesList = [...invitee, ...guests, ...teamMembers];
const seed = `${users[0].username}:${dayjs(reqBody.start).utc().format()}`; const seed = `${users[0].username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const evt: CalendarEvent = { const evt: CalendarEvent = {
@ -353,8 +360,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
selectedCalendars selectedCalendars
); );
const videoBusyTimes = await getBusyVideoTimes(credentials); const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter((time) => time);
calendarBusyTimes.push(...videoBusyTimes); calendarBusyTimes.push(...videoBusyTimes);
console.log("calendarBusyTimes==>>>", calendarBusyTimes);
const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({ const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(), start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
@ -445,6 +453,46 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
} }
//for Daily.co video calls will grab the meeting token for the call
const isDaily = evt.location === "integrations:daily";
let dailyEvent: DailyReturnType;
if (!rescheduleUid) {
dailyEvent = results.filter((ref) => ref.type === "daily")[0]?.createdEvent as DailyReturnType;
} else {
dailyEvent = results.filter((ref) => ref.type === "daily_video")[0]?.updatedEvent as DailyReturnType;
}
let meetingToken;
if (isDaily) {
const response = await fetch("https://api.daily.co/v1/meeting-tokens", {
method: "POST",
body: JSON.stringify({ properties: { room_name: dailyEvent.name, is_owner: true } }),
headers: {
Authorization: "Bearer " + process.env.DAILY_API_KEY,
"Content-Type": "application/json",
},
});
meetingToken = await response.json();
}
//for Daily.co video calls will update the dailyEventReference table
if (isDaily) {
await prisma.dailyEventReference.create({
data: {
dailyurl: dailyEvent.url,
dailytoken: meetingToken.token,
booking: {
connect: {
uid: booking.uid,
},
},
},
});
}
if (eventType.requiresConfirmation && !rescheduleUid) { if (eventType.requiresConfirmation && !rescheduleUid) {
await new EventOrganizerRequestMail(evt, uid).sendEmail(); await new EventOrganizerRequestMail(evt, uid).sendEmail();
} }

View file

@ -11,6 +11,8 @@ import { deleteMeeting } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload"; import sendPayload from "@lib/webhooks/sendPayload";
import getSubscriberUrls from "@lib/webhooks/subscriberUrls"; import getSubscriberUrls from "@lib/webhooks/subscriberUrls";
import { dailyDeleteMeeting } from "../../lib/dailyVideoClient";
export default async function handler(req, res) { export default async function handler(req, res) {
// just bail if it not a DELETE // just bail if it not a DELETE
if (req.method !== "DELETE" && req.method !== "POST") { if (req.method !== "DELETE" && req.method !== "POST") {
@ -37,6 +39,7 @@ export default async function handler(req, res) {
}, },
}, },
attendees: true, attendees: true,
location: true,
references: { references: {
select: { select: {
uid: true, uid: true,
@ -118,6 +121,13 @@ export default async function handler(req, res) {
return await deleteMeeting(credential, bookingRefUid); return await deleteMeeting(credential, bookingRefUid);
} }
} }
//deleting a Daily meeting
const isDaily = bookingToDelete.location === "integrations:daily";
const bookingUID = bookingToDelete.references.filter((ref) => ref.type === "daily")[0]?.uid;
if (isDaily) {
return await dailyDeleteMeeting(credential, bookingUID);
}
}); });
if (bookingToDelete && bookingToDelete.paid) { if (bookingToDelete && bookingToDelete.paid) {

90
pages/call/[uid].tsx Normal file
View file

@ -0,0 +1,90 @@
import DailyIframe from "@daily-co/daily-js";
import { getSession } from "next-auth/client";
import { useRouter } from "next/router";
import { useEffect } from "react";
import prisma from "../../lib/prisma";
export default function JoinCall(props, session) {
const router = useRouter();
//if no booking redirectis to the 404 page
const emptyBooking = props.booking === null;
useEffect(() => {
if (emptyBooking) {
router.push("/call/no-meeting-found");
}
});
useEffect(() => {
if (!emptyBooking && session.userid !== props.booking.user.id) {
const callFrame = DailyIframe.createFrame({
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
});
callFrame.join({
url: props.booking.dailyRef.dailyurl,
showLeaveButton: true,
});
}
if (!emptyBooking && session.userid === props.booking.user.id) {
const callFrame = DailyIframe.createFrame({
showLeaveButton: true,
iframeStyle: {
position: "fixed",
width: "100%",
height: "100%",
},
});
callFrame.join({
url: props.booking.dailyRef.dailyurl,
showLeaveButton: true,
token: props.booking.dailyRef.dailytoken,
});
}
}, []);
return JoinCall;
}
export async function getServerSideProps(context) {
const booking = await prisma.booking.findFirst({
where: {
uid: context.query.uid,
},
select: {
id: true,
user: {
select: {
credentials: true,
},
},
attendees: true,
dailyRef: {
select: {
dailyurl: true,
dailytoken: true,
},
},
references: {
select: {
uid: true,
type: true,
},
},
},
});
const session = await getSession();
return {
props: {
booking: booking,
session: session,
},
};
}

View file

@ -0,0 +1,52 @@
import { XIcon } from "@heroicons/react/outline";
import { ArrowRightIcon } from "@heroicons/react/solid";
import { HeadSeo } from "@components/seo/head-seo";
import Button from "@components/ui/Button";
export default function NoMeetingFound() {
return (
<div>
<HeadSeo title={`No meeting Found`} description={`No Meeting Found`} />
<main className="max-w-3xl mx-auto my-24">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div
className="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
<XIcon className="w-6 h-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
No Meeting Found
</h3>
</div>
<div className="mt-2">
<p className="text-sm text-center text-gray-500">
This meeting does not exist. Contact the meeting owner for an updated link.
</p>
</div>
</div>
<div className="mt-5 text-center sm:mt-6">
<div className="mt-5">
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRightIcon}>
Go back home
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "DailyEventReference" (
"id" SERIAL NOT NULL,
"dailyurl" TEXT NOT NULL DEFAULT E'dailycallurl',
"dailytoken" TEXT NOT NULL DEFAULT E'dailytoken',
"bookingId" INTEGER,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DailyEventReference_bookingId_unique" ON "DailyEventReference"("bookingId");
-- AddForeignKey
ALTER TABLE "DailyEventReference" ADD FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -162,6 +162,14 @@ enum BookingStatus {
PENDING @map("pending") PENDING @map("pending")
} }
model DailyEventReference {
id Int @id @default(autoincrement())
dailyurl String @default("dailycallurl")
dailytoken String @default("dailytoken")
booking Booking? @relation(fields: [bookingId], references: [id])
bookingId Int?
}
model Booking { model Booking {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
uid String @unique uid String @unique
@ -179,6 +187,8 @@ model Booking {
attendees Attendee[] attendees Attendee[]
location String? location String?
dailyRef DailyEventReference?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime? updatedAt DateTime?
confirmed Boolean @default(true) confirmed Boolean @default(true)

123
yarn.lock
View file

@ -282,15 +282,17 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0": "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.0":
version "7.15.4" version "7.15.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/template@^7.12.13", "@babel/template@^7.15.4", "@babel/template@^7.3.3": "@babel/template@^7.12.13", "@babel/template@^7.15.4", "@babel/template@^7.3.3":
version "7.15.4" version "7.15.4"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==
dependencies: dependencies:
"@babel/code-frame" "^7.14.5" "@babel/code-frame" "^7.14.5"
"@babel/parser" "^7.15.4" "@babel/parser" "^7.15.4"
@ -391,6 +393,17 @@
debug "^3.1.0" debug "^3.1.0"
lodash.once "^4.1.1" lodash.once "^4.1.1"
"@daily-co/daily-js@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.16.0.tgz#9020104bb88de62dcc1966e713da65844243b9ab"
integrity sha512-DBWzbZs2IR7uYqfbABva1Ms3f/oX85dnQnCpVbGbexTN63LPIGknFSQp31ZYED88qcG+YJNydywBTb+ApNiNXA==
dependencies:
"@babel/runtime" "^7.12.5"
bowser "^2.8.1"
events "^3.1.0"
fast-equals "^1.6.3"
lodash "^4.17.15"
"@emotion/cache@^11.4.0": "@emotion/cache@^11.4.0":
version "11.4.0" version "11.4.0"
resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz" resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.4.0.tgz"
@ -2478,6 +2491,11 @@ bn.js@^5.0.0, bn.js@^5.1.1:
version "5.2.0" version "5.2.0"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz" resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz"
bowser@^2.8.1:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
@ -3703,7 +3721,7 @@ eventemitter2@^6.4.3:
version "6.4.4" version "6.4.4"
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz" resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz"
events@^3.0.0: events@^3.0.0, events@^3.1.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
@ -3811,6 +3829,11 @@ fast-diff@^1.1.2:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
fast-equals@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
fast-glob@^3.1.1, fast-glob@^3.2.7: fast-glob@^3.1.1, fast-glob@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz"
@ -4359,6 +4382,18 @@ highlight.js@^10.7.1:
version "10.7.3" version "10.7.3"
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz"
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.1: hmac-drbg@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz"
@ -4367,7 +4402,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
dependencies: dependencies:
@ -4826,6 +4861,11 @@ is-windows@^1.0.2:
resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@^1.0.0, isarray@~1.0.0: isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
@ -5795,9 +5835,10 @@ lodash.truncate@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz"
lodash@4.17.21, lodash@4.x, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: lodash@4.17.21, lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.0.0, log-symbols@^4.1.0: log-symbols@^4.0.0, log-symbols@^4.1.0:
version "4.1.0" version "4.1.0"
@ -5815,7 +5856,7 @@ log-update@^4.0.0:
slice-ansi "^4.0.0" slice-ansi "^4.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
dependencies: dependencies:
@ -5949,6 +5990,14 @@ min-document@^2.19.0:
dependencies: dependencies:
dom-walk "^0.1.0" dom-walk "^0.1.0"
mini-create-react-context@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
dependencies:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-svg-data-uri@^1.2.3: mini-svg-data-uri@^1.2.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz" resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz"
@ -6613,6 +6662,13 @@ path-parse@^1.0.6:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^3.0.0: path-type@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@ -7140,7 +7196,7 @@ react-is@17.0.2, react-is@^17.0.1:
version "17.0.2" version "17.0.2"
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
react-is@^16.7.0, react-is@^16.8.1: react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@ -7188,6 +7244,35 @@ react-remove-scroll@^2.4.0:
use-callback-ref "^1.2.3" use-callback-ref "^1.2.3"
use-sidecar "^1.0.1" use-sidecar "^1.0.1"
react-router-dom@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"
integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.1"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d"
integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==
dependencies:
"@babel/runtime" "^7.12.13"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-select@^4.3.1: react-select@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz" resolved "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz"
@ -7371,6 +7456,11 @@ resolve-from@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve@^1.10.0, resolve@^1.20.0: resolve@^1.10.0, resolve@^1.20.0:
version "1.20.0" version "1.20.0"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz"
@ -8063,6 +8153,16 @@ timm@^1.6.1:
resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f"
integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@^1.4.1: tinycolor2@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@ -8430,6 +8530,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
verror@1.10.0: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz"