Compare commits

...
20 Commits
Author SHA1 Message Date
Keith Williams 2236dfe411 v3.4.8
release.yml / release (push) Canceled after 0s
2023-11-09 14:43:25 -03:00
Udit Takkar a67369460a fix: ui fixes 2023-11-09 16:55:37 +00:00
Siddharth Movaliya b2526300a7 fix duplicate labels 2023-11-09 16:55:37 +00:00
Siddharth Movaliya d69395434f fix: Duplicate labels for Destination Calendar 2023-11-09 16:55:37 +00:00
Peer Richelsen 2720c9cc67 added tracking link 2023-11-09 16:36:35 +00:00
Peer Richelsen f1465a15f6 remove dependency 2023-11-09 16:36:35 +00:00
Peer Richelsen 1d24f2654d added deel app 2023-11-09 16:36:35 +00:00
Keith Williams f7eb86577c fix: Sentry warnings for startTransaction (#12300) 2023-11-09 12:55:53 -03:00
Peer Richelsen cc515f00b7 chore: fix avatar margin (#12274) 2023-11-09 15:34:28 +00:00
Morgan e695f75113 fix: create prisma client singleton with immediately invoked function (#12267) 2023-11-09 12:00:19 -03:00
Ash Davis 0d1b6ea5b2 feat: configure availability time picker interval by env variable (#12174) 2023-11-09 09:50:42 -05:00
Joe Au-Yeung 26637cda08 fix: When Installing Calendar App, Set Primary Calendar As Selected Calendar (#12291)
* GCal set primary calendar on install

* Outlook set default calendar as selected cal

* Zoho create selected calendar on install

* Lark create selected cal on install
2023-11-09 14:47:53 +00:00
Morgan 529054aa76 fix: add missing config files to api dockerfile (#12295) 2023-11-09 12:53:37 +00:00
Hariom Balhara 8deee738c2 fix: Recurring Booking - Check for conflicts on first 2 slots only (#11774)
* Add recurring booking tests and fix the bug

* Fix recurring booking tests supporting the new link verification assertions

* Convert tab to spaces
2023-11-09 17:00:51 +05:30
6848362683 feat: 11642 app shimmer video (#12159)
* added initial app

* created basic functionality for Shimmer Video app with tracking of Daily rooms

* changed the type config value in the shimmer video config.json

* re-fixed update to shimmer-video config type

* updated static images for shimmer video app

* fixed tracking Shimmer video event parameter

* Add zod files

* Allow query for "conferencing" apps

* Move to shimmer video

* Redirect to shimmer app

* Remove console.logs

* Remove legacy use of seed-app-store.

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Vik <vsreed@stanford.edu>
Co-authored-by: pathaksarvesh <sarvesh@incrediblevisibility.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2023-11-09 14:03:50 +05:30
Keith WilliamsandCarina Wollendorfer 09fc7e1a4c fix: Remove count from query to update schedule (#12287)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-11-08 14:47:33 -03:00
Carina WollendorferandCarinaWolli 2f73be0ee6 fix: tests for booking limits (#12284)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-08 17:13:56 +00:00
Ryukemeister 3cdf59f2cc fix capitalization typo for show event type message 2023-11-08 14:27:21 +00:00
Morgan 1f9880204e fix: payment cancel button redirects properly (#12254) 2023-11-08 12:34:50 +02:00
Peer Richelsen d0be48ebde chore: delete welcome workflow (#12279) 2023-11-08 09:39:56 +00:00
62 changed files with 1556 additions and 200 deletions
+2
View File
@@ -207,6 +207,8 @@ CSP_POLICY=
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
# Control time intervals on a user's Schedule availability
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
# - ORGANIZATIONS *******************************************************************************************
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
@@ -1,27 +0,0 @@
name: "Welcome new contributors"
on:
issues:
types: opened
pull_request:
types: opened
permissions:
pull-requests: write
issues: write
jobs:
welcome-message:
name: Welcoming New Users
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event.action == 'opened'
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: |-
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
issue-message: |
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
+4
View File
@@ -83,6 +83,10 @@
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
"description": "Set the following value to true if you wish to enable Team Impersonation",
"value": "false"
},
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL": {
"description": "Control time intervals on a user's Schedule availability",
"value": "15"
}
},
"scripts": {
+29 -20
View File
@@ -1,7 +1,7 @@
import type { NextMiddleware } from "next-api-middleware";
import { CONSOLE_URL } from "@calcom/lib/constants";
import { customPrisma } from "@calcom/prisma";
import prisma, { customPrisma } from "@calcom/prisma";
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
@@ -12,32 +12,41 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
} = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!key) {
req.prisma = customPrisma();
req.prisma = prisma;
await next();
return;
}
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
.then((res) => res.json())
.then((res) => res.databaseUrl);
try {
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
.then((res) => res.json())
.then((res) => res.databaseUrl);
if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note:
if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note:
In order to skip verifyApiKey for customPrisma requests,
we pass isAdmin true, and userId 0, if we detect them later,
we skip verifyApiKey logic and pass onto next middleware instead.
*/
req.isAdmin = true;
req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key;
await next();
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
req.isAdmin = true;
req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key;
await next();
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
} catch (err) {
if (req.prisma) {
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
}
throw err;
}
};
@@ -134,7 +134,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
<div className="flex flex-col">
<div className="flex justify-between">
<Label className="font-medium">{t("add_to_calendar")}</Label>
<div>
<Label className="text-emphasis mb-0 font-medium">{t("add_to_calendar")}</Label>
</div>
<Link
href="/apps/categories/calendar"
target="_blank"
@@ -142,21 +144,20 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{t("add_another_calendar")}
</Link>
</div>
<div className="-mt-1 w-full">
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
/>
)}
/>
</div>
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
hideAdvancedText
/>
)}
/>
<p className="text-subtle text-sm">{t("select_which_cal")}</p>
</div>
)}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.4.7",
"version": "3.4.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+23 -18
View File
@@ -13,29 +13,31 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
const session = await getServerSession({ req, res });
const createdBookings: BookingResponse[] = [];
const allRecurringDates: string[] = data.map((booking) => booking.start);
let appsStatus: AppsStatus[] | undefined = undefined;
const appsStatus: AppsStatus[] | undefined = undefined;
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
const numSlotsToCheckForAvailability = 2;
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
// emailed booking taking into account accumulated results to send app status accurately
for (let key = data.length - 1; key >= 0; key--) {
for (let key = 0; key < data.length; key++) {
const booking = data[key];
if (key === 0) {
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
.reduce((prev, curr) => {
if (prev[curr.type]) {
prev[curr.type].failures += curr.failures;
prev[curr.type].success += curr.success;
} else {
prev[curr.type] = curr;
}
return prev;
}, {} as { [key: string]: AppsStatus });
appsStatus = Object.values(calcAppsStatus);
}
// Disable AppStatus in Recurring Booking Email as it requires us to iterate backwards to be able to compute the AppsStatus for all the bookings except the very first slot and then send that slot's email with statuses
// It is also doubtful that how useful is to have the AppsStatus of all the bookings in the email.
// It is more important to iterate forward and check for conflicts for only first few bookings defined by 'numSlotsToCheckForAvailability'
// if (key === 0) {
// const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
// .flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
// .reduce((prev, curr) => {
// if (prev[curr.type]) {
// prev[curr.type].failures += curr.failures;
// prev[curr.type].success += curr.success;
// } else {
// prev[curr.type] = curr;
// }
// return prev;
// }, {} as { [key: string]: AppsStatus });
// appsStatus = Object.values(calcAppsStatus);
// }
const recurringEventReq: NextApiRequest & { userId?: number } = req;
@@ -49,6 +51,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
const eachRecurringBooking = await handleNewBooking(recurringEventReq, {
isNotAnApiCall: true,
skipAvailabilityCheck: key >= numSlotsToCheckForAvailability,
});
createdBookings.push(eachRecurringBooking);
@@ -56,4 +59,6 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
return createdBookings;
}
export const handleRecurringEventBooking = handler;
export default defaultResponder(handler);
@@ -25,6 +25,7 @@ import {
SkeletonContainer,
SkeletonText,
showToast,
Label,
} from "@calcom/ui";
import { Plus, Calendar } from "@calcom/ui/components/icon";
@@ -113,17 +114,20 @@ const CalendarsView = () => {
<h2 className="text-emphasis mb-1 text-base font-bold leading-5 tracking-wide">
{t("add_to_calendar")}
</h2>
<p className="text-default text-sm leading-tight">{t("add_to_calendar_description")}</p>
<p className="text-subtle text-sm leading-tight">{t("add_to_calendar_description")}</p>
</div>
<div className="border-subtle flex w-full flex-col space-y-3 border border-x border-y-0 px-4 py-6 sm:px-6">
<DestinationCalendarSelector
hidePlaceholder
value={selectedDestinationCalendarOption?.externalId}
onChange={(option) => {
setSelectedDestinationCalendar(option);
}}
isLoading={mutation.isLoading}
/>
<div>
<Label className="text-default mb-0 font-medium">{t("add_events_to")}</Label>
<DestinationCalendarSelector
hidePlaceholder
value={selectedDestinationCalendarOption?.externalId}
onChange={(option) => {
setSelectedDestinationCalendar(option);
}}
isLoading={mutation.isLoading}
/>
</div>
</div>
<SectionBottomActions align="end">
<Button
@@ -1232,7 +1232,7 @@
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on Profile",
"show_eventtype_on_profile": "Show on profile",
"embed": "Embed",
"new_username": "New username",
"current_username": "Current username",
-1
View File
@@ -2,5 +2,4 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0,
});
+9 -9
View File
@@ -929,7 +929,7 @@ describe("getSchedule", () => {
date: null,
},
],
timeZone: Timezones["+5:30"],
timeZone: Timezones["+6:00"],
},
],
},
@@ -938,15 +938,15 @@ describe("getSchedule", () => {
{
userId: 101,
eventTypeId: 1,
startTime: `${plus2DateString}T08:30:00.000Z`,
endTime: `${plus2DateString}T08:29:59.999Z`,
startTime: `${plus2DateString}T08:00:00.000Z`,
endTime: `${plus2DateString}T09:00:00.000Z`,
status: "ACCEPTED" as BookingStatus,
},
{
userId: 101,
eventTypeId: 2,
startTime: `${plus2DateString}T08:30:00.000Z`,
endTime: `${plus2DateString}T08:29:59.999Z`,
startTime: `${plus2DateString}T08:00:00.000Z`,
endTime: `${plus2DateString}T09:00:00.000Z`,
status: "ACCEPTED" as BookingStatus,
},
],
@@ -960,7 +960,7 @@ describe("getSchedule", () => {
eventTypeSlug: "",
startTime: `${plus1DateString}T00:00:00.000Z`,
endTime: `${plus3DateString}T23:59:59.999Z`,
timeZone: Timezones["+5:30"],
timeZone: Timezones["+6:00"],
isTeamEvent: false,
},
});
@@ -971,7 +971,7 @@ describe("getSchedule", () => {
eventTypeSlug: "",
startTime: `${plus1DateString}T00:00:00.000Z`,
endTime: `${plus3DateString}T23:59:59.999Z`,
timeZone: Timezones["+5:30"],
timeZone: Timezones["+6:00"],
isTeamEvent: false,
},
});
@@ -979,7 +979,7 @@ describe("getSchedule", () => {
let availableSlotsInTz: dayjs.Dayjs[] = [];
for (const date in thisUserAvailabilityBookingLimitOne.slots) {
thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => {
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
});
}
@@ -988,7 +988,7 @@ describe("getSchedule", () => {
availableSlotsInTz = [];
for (const date in thisUserAvailabilityBookingLimitTwo.slots) {
thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => {
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"]));
});
}
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that
+26 -20
View File
@@ -348,16 +348,19 @@ export function expectSuccessfulBookingCreationEmails({
titleTag: "confirmed_event_type_subject",
heading: recurrence ? "new_event_scheduled_recurring" : "new_event_scheduled",
subHeading: "",
links: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
text: "reschedule",
},
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
text: "cancel",
},
],
links: recurrence
? [
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=true`,
text: "cancel",
},
]
: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
text: "reschedule",
},
],
...(bookingTimeRange
? {
bookingTimeRange: {
@@ -396,16 +399,19 @@ export function expectSuccessfulBookingCreationEmails({
iCalUID: iCalUID,
recurrence,
},
links: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
text: "reschedule",
},
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
text: "cancel",
},
],
links: recurrence
? [
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=true`,
text: "cancel",
},
]
: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
text: "reschedule",
},
],
},
`${booker.name} <${booker.email}>`
);
+3
View File
@@ -48,6 +48,9 @@ RUN apt-get update -qq && \
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
COPY --from=build /app/apps/api/next-i18next.config.js ./apps/api/next-i18next.config.js
COPY --from=build /app/apps/api/next.config.js ./apps/api/next.config.js
COPY --from=build /app/apps/api/tsconfig.json ./apps/api/tsconfig.json
COPY --from=build /app/apps/api/.next ./apps/api/.next
COPY --from=build /app/apps/api/.turbo ./apps/api/.turbo
COPY --from=build /app/turbo.json ./turbo.json
@@ -23,6 +23,7 @@ import { appKeysSchema as plausible_zod_ts } from "./plausible/zod";
import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
@@ -58,6 +59,7 @@ export const appKeysSchemas = {
qr_code: qr_code_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
@@ -13,6 +13,7 @@ import campfire_config_json from "./campfire/config.json";
import closecom_config_json from "./closecom/config.json";
import cron_config_json from "./cron/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import deel_config_json from "./deel/config.json";
import discord_config_json from "./discord/config.json";
import eightxeight_config_json from "./eightxeight/config.json";
import element_call_config_json from "./element-call/config.json";
@@ -47,6 +48,7 @@ import riverside_config_json from "./riverside/config.json";
import routing_forms_config_json from "./routing-forms/config.json";
import salesforce_config_json from "./salesforce/config.json";
import sendgrid_config_json from "./sendgrid/config.json";
import shimmervideo_config_json from "./shimmervideo/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import skiff_config_json from "./skiff/config.json";
@@ -87,6 +89,7 @@ export const appStoreMetadata = {
closecom: closecom_config_json,
cron: cron_config_json,
dailyvideo: dailyvideo__metadata_ts,
deel: deel_config_json,
discord: discord_config_json,
eightxeight: eightxeight_config_json,
"element-call": element_call_config_json,
@@ -121,6 +124,7 @@ export const appStoreMetadata = {
"routing-forms": routing_forms_config_json,
salesforce: salesforce_config_json,
sendgrid: sendgrid_config_json,
shimmervideo: shimmervideo_config_json,
signal: signal_config_json,
sirius_video: sirius_video_config_json,
skiff: skiff_config_json,
@@ -23,6 +23,7 @@ import { appDataSchema as plausible_zod_ts } from "./plausible/zod";
import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
@@ -58,6 +59,7 @@ export const appDataSchemas = {
qr_code: qr_code_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
@@ -13,6 +13,7 @@ export const apiHandlers = {
campfire: import("./campfire/api"),
closecom: import("./closecom/api"),
cron: import("./cron/api"),
deel: import("./deel/api"),
discord: import("./discord/api"),
eightxeight: import("./eightxeight/api"),
"element-call": import("./element-call/api"),
@@ -47,6 +48,7 @@ export const apiHandlers = {
"routing-forms": import("./routing-forms/api"),
salesforce: import("./salesforce/api"),
sendgrid: import("./sendgrid/api"),
shimmervideo: import("./shimmervideo/api"),
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),
skiff: import("./skiff/api"),
@@ -21,6 +21,7 @@ import office365video_config_json from "./office365video/config.json";
import ping_config_json from "./ping/config.json";
import plausible_config_json from "./plausible/config.json";
import riverside_config_json from "./riverside/config.json";
import shimmervideo_config_json from "./shimmervideo/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
@@ -53,6 +54,7 @@ export const appStoreMetadata = {
ping: ping_config_json,
plausible: plausible_config_json,
riverside: riverside_config_json,
shimmervideo: shimmervideo_config_json,
signal: signal_config_json,
sirius_video: sirius_video_config_json,
sylapsvideo: sylapsvideo_config_json,
+8
View File
@@ -0,0 +1,8 @@
---
items:
- 1.jpeg
- 2.jpeg
- 3.jpeg
---
{DESCRIPTION}
+20
View File
@@ -0,0 +1,20 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "https://go.cal.com/deel",
},
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";
+17
View File
@@ -0,0 +1,17 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Deel",
"slug": "deel",
"type": "deel_other",
"logo": "icon.svg",
"url": "https://go.cal.com/deel",
"variant": "other",
"categories": ["other"],
"publisher": "Deel.com",
"email": "support@deel.com",
"description": "Integrate Deel with your Calendar and get automatic updates on key employee dates like PTO, start days, public holidays, and birthdays. No manual entry. No errors. Just a calendar that's always in sync with your team.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "link-as-an-app",
"dirName": "deel"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/deel",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Integrate Deel with your Calendar and get automatic updates on key employee dates like PTO, start days, public holidays, and birthdays. No manual entry. No errors. Just a calendar that's always in sync with your team."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg width="2484" height="2484" viewBox="0 0 2484 2484" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="2484" height="2484" rx="200" fill="#1D3377"/>
<path d="M1771.45 1522.76C1769 1522.76 1767.01 1520.76 1767.01 1518.31V982.424C1767.01 980.317 1768.49 978.499 1770.54 978.069L1863.22 958.731C1865.97 958.156 1868.56 960.265 1868.56 963.086V1518.31C1868.56 1520.76 1866.57 1522.76 1864.12 1522.76H1771.45Z" fill="white"/>
<path d="M606.578 1534.09C569.555 1534.09 536.498 1525.02 507.409 1506.87C478.319 1488.72 455.312 1463.9 438.387 1432.4C421.462 1400.91 413 1365.15 413 1325.11C413 1285.08 421.462 1249.58 438.387 1218.62C455.312 1187.12 478.319 1162.57 507.409 1144.95C536.498 1126.8 569.555 1117.73 606.578 1117.73C636.196 1117.73 662.113 1123.33 684.326 1134.54C702.132 1143.53 717.22 1155.43 729.587 1170.24C732.391 1173.6 738.274 1171.7 738.274 1167.32V972.788C738.274 970.68 739.75 968.862 741.807 968.433L834.48 949.095C837.236 948.52 839.824 950.629 839.824 953.45V1520.04C839.824 1522.49 837.835 1524.48 835.386 1524.48H753.033C750.913 1524.48 749.086 1522.98 748.679 1520.89L740.24 1477.69C739.492 1473.87 734.492 1472.74 732.014 1475.74C720.188 1490.04 705.351 1502.81 687.499 1514.07C666.872 1527.42 639.898 1534.09 606.578 1534.09ZM627.998 1444.41C660.79 1444.41 687.499 1433.47 708.126 1411.59C729.282 1389.17 739.86 1360.61 739.86 1325.91C739.86 1291.21 729.282 1262.92 708.126 1241.04C687.499 1218.62 660.79 1207.41 627.998 1207.41C595.735 1207.41 569.026 1218.35 547.87 1240.24C526.714 1262.12 516.136 1290.41 516.136 1325.11C516.136 1359.81 526.714 1388.37 547.87 1410.78C569.026 1433.2 595.735 1444.41 627.998 1444.41Z" fill="white"/>
<path d="M1087.44 1534.16C1047.78 1534.16 1012.6 1525.61 981.928 1508.53C951.252 1491.45 927.186 1467.43 909.732 1436.47C892.28 1405.51 883.551 1369.75 883.551 1329.18C883.551 1288.07 892.014 1251.51 908.94 1219.48C926.394 1187.45 950.194 1162.63 980.342 1145.02C1011.02 1126.87 1046.98 1117.79 1088.24 1117.79C1126.85 1117.79 1160.96 1126.33 1190.58 1143.41C1220.2 1160.5 1243.21 1183.98 1259.6 1213.88C1276.53 1243.23 1284.99 1276.06 1284.99 1312.36C1284.99 1318.23 1284.72 1324.37 1284.19 1330.78C1284.19 1335.91 1284.03 1341.21 1283.68 1346.69C1283.54 1349.01 1281.61 1350.8 1279.29 1350.8H989.061C986.49 1350.8 984.45 1352.98 984.704 1355.55C987.54 1384.25 997.987 1406.95 1016.04 1423.66C1035.61 1441.28 1059.15 1450.08 1086.65 1450.08C1107.28 1450.08 1124.47 1445.54 1138.22 1436.47C1151.67 1427.42 1161.84 1416 1168.72 1402.21C1169.5 1400.65 1171.06 1399.64 1172.79 1399.64H1267.15C1270.11 1399.64 1272.24 1402.5 1271.31 1405.32C1263.76 1428.2 1251.93 1449.26 1235.8 1468.5C1218.88 1488.78 1197.72 1504.8 1172.33 1516.54C1147.47 1528.28 1119.18 1534.16 1087.44 1534.16ZM1088.24 1201.07C1063.38 1201.07 1041.43 1208.27 1022.39 1222.68C1004.52 1235.71 992.706 1255.32 986.948 1281.5C986.353 1284.22 988.452 1286.74 991.225 1286.74H1177.14C1179.69 1286.74 1181.73 1284.58 1181.47 1282.03C1179.08 1258.07 1169.68 1238.82 1153.29 1224.29C1135.84 1208.81 1114.15 1201.07 1088.24 1201.07Z" fill="white"/>
<path d="M1529.21 1534.16C1489.54 1534.16 1454.37 1525.61 1423.69 1508.53C1393.02 1491.45 1368.95 1467.43 1351.5 1436.47C1334.04 1405.51 1325.32 1369.75 1325.32 1329.18C1325.32 1288.07 1333.78 1251.51 1350.7 1219.48C1368.16 1187.45 1391.96 1162.63 1422.11 1145.02C1452.78 1126.87 1488.75 1117.79 1530 1117.79C1568.61 1117.79 1602.73 1126.33 1632.34 1143.41C1661.96 1160.5 1684.97 1183.98 1701.37 1213.88C1718.29 1243.23 1726.75 1276.06 1726.75 1312.36C1726.75 1318.23 1726.49 1324.37 1725.96 1330.78C1725.96 1335.91 1725.79 1341.21 1725.45 1346.69C1725.31 1349.01 1723.37 1350.8 1721.05 1350.8H1430.82C1428.26 1350.8 1426.22 1352.98 1426.47 1355.55C1429.31 1384.25 1439.75 1406.95 1457.81 1423.66C1477.38 1441.28 1500.91 1450.08 1528.41 1450.08C1549.04 1450.08 1566.23 1445.54 1579.98 1436.47C1593.44 1427.42 1603.61 1416 1610.49 1402.21C1611.26 1400.65 1612.83 1399.64 1614.56 1399.64H1708.91C1711.88 1399.64 1714.01 1402.5 1713.08 1405.32C1705.53 1428.2 1693.69 1449.26 1677.57 1468.5C1660.64 1488.78 1639.48 1504.8 1614.1 1516.54C1589.24 1528.28 1560.94 1534.16 1529.21 1534.16ZM1530 1201.07C1505.14 1201.07 1483.19 1208.27 1464.15 1222.68C1446.29 1235.71 1434.47 1255.32 1428.71 1281.5C1428.12 1284.22 1430.21 1286.74 1432.99 1286.74H1618.9C1621.46 1286.74 1623.49 1284.58 1623.24 1282.03C1620.84 1258.07 1611.45 1238.82 1595.06 1224.29C1577.6 1208.81 1555.92 1201.07 1530 1201.07Z" fill="white"/>
<path d="M2001.63 1522.87C2039.39 1522.87 2070 1491.98 2070 1453.87C2070 1415.76 2039.39 1384.87 2001.63 1384.87C1963.87 1384.87 1933.27 1415.76 1933.27 1453.87C1933.27 1491.98 1963.87 1522.87 2001.63 1522.87Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -41,7 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const token = await oAuth2Client.getToken(code);
key = token.res?.data;
await prisma.credential.create({
const credential = await prisma.credential.create({
data: {
type: "google_calendar",
key,
@@ -49,6 +49,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
appId: "google-calendar",
},
});
// Set the primary calendar as the first selected calendar
// We can ignore this type error because we just validated the key when we init oAuth2Client
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
oAuth2Client.setCredentials(key);
const calendar = google.calendar({
version: "v3",
auth: oAuth2Client,
});
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
const primaryCal = cals.data.items?.find((cal) => cal.primary);
if (primaryCal?.id) {
await prisma.selectedCalendar.create({
data: {
userId: req.session.user.id,
externalId: primaryCal.id,
credentialId: credential.id,
integration: "google_calendar",
},
});
}
}
if (state?.installGoogleVideo) {
+1
View File
@@ -37,6 +37,7 @@ const appStore = {
"zoho-bigin": () => import("./zoho-bigin"),
basecamp3: () => import("./basecamp3"),
telegramvideo: () => import("./telegram"),
shimmervideo: () => import("./shimmervideo"),
};
export default appStore;
@@ -89,6 +89,32 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
});
}
const primaryCalendarResponse = await fetch(
`https://${LARK_HOST}/open-apis/calendar/v4/calendars/primary`,
{
method: "GET",
headers: {
Authorization: `Bearer ${key.access_token}`,
"Content-Type": "application/json",
},
}
);
if (primaryCalendarResponse.status === 200) {
const primaryCalendar = await primaryCalendarResponse.json();
if (primaryCalendar.data.calendars.calendar.calendar_id && req.session?.user?.id) {
await prisma.selectedCalendar.create({
data: {
userId: req.session?.user.id,
integration: "lark_calendar",
externalId: primaryCalendar.data.calendars.calendar.calendar_id as string,
credentialId: currentCredential?.id,
},
});
}
}
res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
getInstalledAppPath({ variant: "calendar", slug: "lark-calendar" })
@@ -1,6 +1,8 @@
import type { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { handleErrorsJson } from "@calcom/lib/errors";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
@@ -65,7 +67,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
delete responseBody.expires_in;
await prisma.credential.create({
const credential = await prisma.credential.create({
data: {
type: "office365_calendar",
key: responseBody,
@@ -74,6 +76,50 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
// Set the isDefaultCalendar as selectedCalendar
// If a user has multiple calendars, keep on making calls until we find the default calendar
let defaultCalendar: OfficeCalendar | undefined = undefined;
let requestUrl = "https://graph.microsoft.com/v1.0/me/calendars?$select=id,isDefaultCalendar";
let finishedParsingCalendars = false;
while (!finishedParsingCalendars) {
const calRequest = await fetch(requestUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${responseBody.access_token}`,
"Content-Type": "application/json",
},
});
let calBody = await handleErrorsJson<{ value: OfficeCalendar[]; "@odata.nextLink"?: string }>(calRequest);
if (typeof responseBody === "string") {
calBody = JSON.parse(responseBody) as { value: OfficeCalendar[] };
}
const findDefaultCalendar = calBody.value.find((calendar) => calendar.isDefaultCalendar);
if (findDefaultCalendar) {
defaultCalendar = findDefaultCalendar;
}
if (calBody["@odata.nextLink"]) {
requestUrl = calBody["@odata.nextLink"];
} else {
finishedParsingCalendars = true;
}
}
if (defaultCalendar?.id && req.session?.user?.id) {
await prisma.selectedCalendar.create({
data: {
userId: req.session?.user.id,
integration: "office365_calendar",
externalId: defaultCalendar.id,
credentialId: credential.id,
},
});
}
const state = decodeOAuthState(req);
return res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
@@ -0,0 +1,7 @@
---
items:
- 1.jpeg
- 2.jpeg
---
{DESCRIPTION}
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,23 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Shimmer Video",
"slug": "shimmervideo",
"type": "shimmer_video",
"logo": "icon.png",
"url": "https://shimmer.care",
"variant": "conferencing",
"categories": ["conferencing"],
"publisher": "Shimmer.care",
"email": "support@shimmer.care",
"description": "The #1 Expert ADHD Coach. Weekly calls and in-app support so that you can reach your full potential",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"appData": {
"location": {
"linkType": "dynamic",
"type": "integrations:shimmer_video",
"label": "Shimmer Video"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";
@@ -0,0 +1,195 @@
import { z } from "zod";
import { handleErrorsJson } from "@calcom/lib/errors";
import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import { getRecordingsResponseSchema, getAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
import { getShimmerAppKeys } from "./getShimmerAppKeys";
/** Shimmer Video app type in the config.json
* changed to 'shimmer_video' to support video conferencing
*/
/** @link https://docs.daily.co/reference/rest-api/rooms/create-room */
const dailyReturnTypeSchema = z.object({
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
id: z.string(),
/** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */
name: z.string(),
api_created: z.boolean(),
privacy: z.union([z.literal("private"), z.literal("public")]),
/** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */
url: z.string(),
created_at: z.string(),
config: z.object({
enable_prejoin_ui: z.boolean(),
enable_people_ui: z.boolean(),
enable_emoji_reactions: z.boolean(),
enable_pip_ui: z.boolean(),
enable_hand_raising: z.boolean(),
enable_network_ui: z.boolean(),
enable_video_processing_ui: z.boolean(),
enable_noise_cancellation_ui: z.boolean(),
enable_advanced_chat: z.boolean(),
//above flags are for prebuilt daily
enable_chat: z.boolean(),
enable_knocking: z.boolean(),
}),
});
export interface DailyEventResult {
id: string;
name: string;
api_created: boolean;
privacy: string;
url: string;
created_at: string;
config: Record<string, unknown>;
}
export interface DailyVideoCallData {
type: string;
id: string;
password: string;
url: string;
}
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key } = await getShimmerAppKeys();
const response = await fetch(`https://api.daily.co/v1${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
return response;
};
export const fetcherShimmer = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key, api_route } = await getShimmerAppKeys();
if (!api_route) {
//if no api_route, then we wont push to shimmer
return Promise.resolve([]);
}
const response = await fetch(`${api_route}${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
});
return response;
};
export const postToShimmerAPI = async (
event: CalendarEvent,
endpoint: string,
body: Record<string, unknown>
) => {
return fetcherShimmer(endpoint, {
method: "POST",
body: JSON.stringify({
cal: event,
daily: body,
}),
});
};
function postToDailyAPI(endpoint: string, body: Record<string, unknown>) {
return fetcher(endpoint, {
method: "POST",
body: JSON.stringify(body),
});
}
const ShimmerDailyVideoApiAdapter = (): VideoApiAdapter => {
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise<VideoCallData> {
if (!event.uid) {
throw new Error("We need need the booking uid to create the Daily reference in DB");
}
const body = await translateEvent();
const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse);
// const meetingToken = await postToDailyAPI("/meeting-tokens", {
// properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true },
// }).then(meetingTokenSchema.parse);
await postToShimmerAPI(event, "trackDailyRoom", dailyEvent);
return Promise.resolve({
type: "shimmer_video",
id: dailyEvent.name,
password: "",
// password: meetingToken.token,
url: `https://app.shimmer.care?videoId=${dailyEvent.name}`,
});
}
const translateEvent = async () => {
return {
privacy: "private",
properties: {
enable_prejoin_ui: true,
enable_people_ui: true,
enable_emoji_reactions: true,
enable_pip_ui: true,
enable_hand_raising: true,
enable_network_ui: true,
enable_video_processing_ui: true,
enable_noise_cancellation_ui: true,
enable_advanced_chat: true,
//above flags are for prebuilt daily
enable_knocking: true,
enable_screenshare: true,
enable_chat: true,
},
};
};
return {
/** Daily doesn't need to return busy times, so we return empty */
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> =>
createOrUpdateMeeting("/rooms", event),
deleteMeeting: async (uid: string): Promise<void> => {
await fetcher(`/rooms/${uid}`, { method: "DELETE" });
return Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> =>
createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event),
getRecordings: async (roomName: string): Promise<GetRecordingsResponseSchema> => {
try {
const res = await fetcher(`/recordings?room_name=${roomName}`).then(
getRecordingsResponseSchema.parse
);
return Promise.resolve(res);
} catch (err) {
throw new Error("Something went wrong! Unable to get recording");
}
},
getRecordingDownloadLink: async (recordingId: string): Promise<GetAccessLinkResponseSchema> => {
try {
const res = await fetcher(`/recordings/${recordingId}/access-link?valid_for_secs=172800`).then(
getAccessLinkResponseSchema.parse
);
return Promise.resolve(res);
} catch (err) {
console.log("err", err);
throw new Error("Something went wrong! Unable to get recording access link");
}
},
};
};
export default ShimmerDailyVideoApiAdapter;
@@ -0,0 +1,13 @@
import { z } from "zod";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
const shimmerAppKeysSchema = z.object({
api_key: z.string(),
api_route: z.string(),
});
export const getShimmerAppKeys = async () => {
const appKeys = await getAppKeysFromSlug("shimmer-video");
return shimmerAppKeysSchema.parse(appKeys);
};
@@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/shimmer-video",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "The #1 Expert ADHD Coach. Weekly calls and in-app support so that you can reach your full potential"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

+8
View File
@@ -0,0 +1,8 @@
import { z } from "zod";
export const appKeysSchema = z.object({
api_key: z.string(),
api_route: z.string(),
});
export const appDataSchema = z.object({});
@@ -5,10 +5,10 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import config from "../config.json";
import type { ZohoAuthCredentials } from "../types/ZohoCalendar";
@@ -64,7 +64,36 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
expires_in: Math.round(+new Date() / 1000 + responseBody.expires_in),
};
await createOAuthAppCredential({ appId: config.slug, type: config.type }, key, req);
const credential = await prisma.credential.create({
data: {
type: config.type,
key,
userId: req.session.user.id,
appId: config.slug,
},
});
const calendarResponse = await fetch("https://calendar.zoho.com/api/v1/calendars", {
method: "GET",
headers: {
Authorization: `Bearer ${key.access_token}`,
"Content-Type": "application/json",
},
});
const data = await calendarResponse.json();
const primaryCalendar = data.calendars.find((calendar: any) => calendar.isdefault);
if (primaryCalendar.uid) {
await prisma.selectedCalendar.create({
data: {
userId: req.session.user.id,
integration: config.type,
externalId: primaryCalendar.uid,
credentialId: credential.id,
},
});
}
res.redirect(
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: config.variant, slug: config.slug })
+1 -2
View File
@@ -97,7 +97,7 @@ export default class EventManager {
// (type closecom_other_calendar)
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
this.videoCredentials = appCredentials
.filter((cred) => cred.type.endsWith("_video"))
.filter((cred) => cred.type.endsWith("_video") || cred.type.endsWith("_conferencing"))
// Whenever a new video connection is added, latest credentials are added with the highest ID.
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
// We also don't have updatedAt or createdAt dates on credentials so this is the best we can do
@@ -654,7 +654,6 @@ export default class EventManager {
*/
private async createVideoEvent(event: CalendarEvent) {
const credential = this.getVideoCredential(event);
if (credential) {
return createMeeting(credential, event);
} else {
@@ -41,7 +41,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
<UserAvatarGroup size="sm" className="border-muted" users={shownUsers} />
)}
<p className="text-subtle text-sm font-semibold">
<p className="text-subtle mt-2 text-sm font-semibold">
{showOnlyProfileName
? profile.name
: shownUsers
@@ -364,11 +364,7 @@ async function ensureAvailableUsers(
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
users: IsFixedAwareUser[];
},
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
recurringDatesInfo?: {
allRecurringDates: string[] | undefined;
currentRecurringIndex: number | undefined;
}
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }
) {
const availableUsers: IsFixedAwareUser[] = [];
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
@@ -623,10 +619,13 @@ async function handler(
req: NextApiRequest & { userId?: number | undefined },
{
isNotAnApiCall = false,
skipAvailabilityCheck = false,
}: {
isNotAnApiCall?: boolean;
skipAvailabilityCheck?: boolean;
} = {
isNotAnApiCall: false,
skipAvailabilityCheck: false,
}
) {
const { userId } = req;
@@ -705,6 +704,7 @@ async function handler(
isTeamEventType,
eventType: getPiiFreeEventType(eventType),
dynamicUserList,
skipAvailabilityCheck,
paymentAppData: {
enabled: paymentAppData.enabled,
price: paymentAppData.price,
@@ -907,7 +907,7 @@ async function handler(
}
}
if (!eventType.seatsPerTimeSlot) {
if (!eventType.seatsPerTimeSlot && !skipAvailabilityCheck) {
const availableUsers = await ensureAvailableUsers(
{
...eventType,
@@ -924,10 +924,6 @@ async function handler(
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
{
allRecurringDates,
currentRecurringIndex,
}
);
@@ -0,0 +1,819 @@
import { v4 as uuidv4 } from "uuid";
import { describe, expect } from "vitest";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
import {
createBookingScenario,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
mockSuccessfulVideoMeetingCreation,
mockCalendarToHaveNoBusySlots,
getDate,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import {
expectWorkflowToBeTriggered,
expectSuccessfulBookingCreationEmails,
expectBookingToBeInDatabase,
expectBookingCreatedWebhookToHaveBeenFired,
expectSuccessfulCalendarEventCreationInCalendar,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { createMockNextJsRequest } from "./lib/createMockNextJsRequest";
import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking";
import { setupAndTeardown } from "./lib/setupAndTeardown";
const DAY_IN_MS = 1000 * 60 * 60 * 24;
function getPlusDayDate(date: string, days: number) {
return new Date(new Date(date).getTime() + days * DAY_IN_MS);
}
// Local test runs sometime gets too slow
const timeout = process.env.CI ? 5000 : 20000;
describe("handleNewBooking", () => {
setupAndTeardown();
describe("Recurring EventType:", () => {
test(
`should create successful bookings for the number of slots requested
1. Should create the same number of bookings as requested slots in the database
2. Should send emails for the first booking only to the booker as well as organizer
3. Should create a calendar event for every booking in the destination calendar
3. Should trigger BOOKING_CREATED webhook for every booking
`,
async ({ emails }) => {
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
.handleRecurringEventBooking;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
},
});
const recurrence = getRecurrence({
type: "weekly",
numberOfOccurrences: 3,
});
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
recurringEvent: recurrence,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
},
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const recurringCountInRequest = 2;
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:30:00.000Z`,
recurringEventId: uuidv4(),
recurringCount: recurringCountInRequest,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const numOfSlotsToBeBooked = 4;
const { req, res } = createMockNextJsRequest({
method: "POST",
body: Array(numOfSlotsToBeBooked)
.fill(mockBookingData)
.map((mockBookingData, index) => {
return {
...mockBookingData,
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
};
}),
});
const createdBookings = await handleRecurringEventBooking(req, res);
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
for (const [index, createdBooking] of Object.entries(createdBookings)) {
logger.debug("Assertion for Booking with index:", index, { createdBooking });
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
recurringEventId: mockBookingData.recurringEventId,
references: [
{
type: "daily_video",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
type: "google_calendar",
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
],
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
location: "integrations:daily",
subscriberUrl: "http://my-webhook.example.com",
//FIXME: All recurring bookings seem to have the same URL. https://github.com/calcom/cal.com/issues/11955
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
});
}
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({
booker,
booking: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBookings[0].uid!,
urlOrigin: WEBAPP_URL,
},
organizer,
emails,
bookingTimeRange: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
start: createdBookings[0].startTime!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
end: createdBookings[0].endTime!,
},
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
recurrence: {
...recurrence,
count: recurringCountInRequest,
},
});
expect(emails.get().length).toBe(2);
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
]);
},
timeout
);
test(
`should fail recurring booking if second slot is already booked`,
async ({}) => {
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
.handleRecurringEventBooking;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
},
});
const recurrence = getRecurrence({
type: "weekly",
numberOfOccurrences: 3,
});
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
const plus2DateString = getDate({ dateIncrement: 2 }).dateString;
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
recurringEvent: recurrence,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
},
},
],
bookings: [
{
uid: "booking-1-uid",
eventTypeId: 1,
userId: organizer.id,
status: BookingStatus.ACCEPTED,
startTime: `${plus2DateString}T04:00:00.000Z`,
endTime: `${plus2DateString}T04:30:00.000Z`,
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const recurringCountInRequest = 2;
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:30:00.000Z`,
recurringEventId: uuidv4(),
recurringCount: recurringCountInRequest,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const numOfSlotsToBeBooked = 4;
const { req, res } = createMockNextJsRequest({
method: "POST",
body: Array(numOfSlotsToBeBooked)
.fill(mockBookingData)
.map((mockBookingData, index) => {
return {
...mockBookingData,
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
};
}),
});
expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow("No available users found");
// Actually the first booking goes through in this case but the status is still a failure. We should do a dry run to check if booking is possible for the 2 slots and if yes, then only go for the actual booking otherwise fail the recurring bookign
},
timeout
);
test(
`should create successful bookings for the number of slots requested even if the third slot is already booked as long as first two slots are free
1. Should create the same number of bookings as requested slots in the database
2. Should send emails for the first booking only to the booker as well as organizer
3. Should create a calendar event for every booking in the destination calendar
3. Should trigger BOOKING_CREATED webhook for every booking
`,
async ({ emails }) => {
const recurringCountInRequest = 4;
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
.handleRecurringEventBooking;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
},
});
const recurrence = getRecurrence({
type: "weekly",
numberOfOccurrences: 3,
});
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
const plus3DateString = getDate({ dateIncrement: 3 }).dateString;
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
recurringEvent: recurrence,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
},
},
],
bookings: [
{
uid: "booking-1-uid",
eventTypeId: 1,
userId: organizer.id,
status: BookingStatus.ACCEPTED,
startTime: `${plus3DateString}T04:00:00.000Z`,
endTime: `${plus3DateString}T04:30:00.000Z`,
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:30:00.000Z`,
recurringEventId: uuidv4(),
recurringCount: recurringCountInRequest,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const numOfSlotsToBeBooked = 4;
const { req, res } = createMockNextJsRequest({
method: "POST",
body: Array(numOfSlotsToBeBooked)
.fill(mockBookingData)
.map((mockBookingData, index) => {
return {
...mockBookingData,
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
};
}),
});
const createdBookings = await handleRecurringEventBooking(req, res);
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
for (const [index, createdBooking] of Object.entries(createdBookings)) {
logger.debug("Assertion for Booking with index:", index, { createdBooking });
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
recurringEventId: mockBookingData.recurringEventId,
references: [
{
type: "daily_video",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
type: "google_calendar",
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
],
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
location: "integrations:daily",
subscriberUrl: "http://my-webhook.example.com",
//FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed.
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
});
}
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({
booker,
booking: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBookings[0].uid!,
urlOrigin: WEBAPP_URL,
},
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
recurrence: {
...recurrence,
count: recurringCountInRequest,
},
});
expect(emails.get().length).toBe(2);
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
]);
},
timeout
);
test(
`should create successful bookings for the number of slots requested even if the last slot is already booked as long as first two slots are free
1. Should create the same number of bookings as requested slots in the database
2. Should send emails for the first booking only to the booker as well as organizer
3. Should create a calendar event for every booking in the destination calendar
3. Should trigger BOOKING_CREATED webhook for every booking
`,
async ({ emails }) => {
const recurringCountInRequest = 4;
const handleRecurringEventBooking = (await import("@calcom/web/pages/api/book/recurring-event"))
.handleRecurringEventBooking;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
},
});
const recurrence = getRecurrence({
type: "weekly",
numberOfOccurrences: 3,
});
const plus1DateString = getDate({ dateIncrement: 1 }).dateString;
const plus4DateString = getDate({ dateIncrement: 4 }).dateString;
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
recurringEvent: recurrence,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
},
},
],
bookings: [
{
uid: "booking-1-uid",
eventTypeId: 1,
userId: organizer.id,
status: BookingStatus.ACCEPTED,
startTime: `${plus4DateString}T04:00:00.000Z`,
endTime: `${plus4DateString}T04:30:00.000Z`,
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
start: `${plus1DateString}T04:00:00.000Z`,
end: `${plus1DateString}T04:30:00.000Z`,
recurringEventId: uuidv4(),
recurringCount: recurringCountInRequest,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const numOfSlotsToBeBooked = 4;
const { req, res } = createMockNextJsRequest({
method: "POST",
body: Array(numOfSlotsToBeBooked)
.fill(mockBookingData)
.map((mockBookingData, index) => {
return {
...mockBookingData,
start: getPlusDayDate(mockBookingData.start, index).toISOString(),
end: getPlusDayDate(mockBookingData.end, index).toISOString(),
};
}),
});
const createdBookings = await handleRecurringEventBooking(req, res);
expect(createdBookings.length).toBe(numOfSlotsToBeBooked);
for (const [index, createdBooking] of Object.entries(createdBookings)) {
logger.debug("Assertion for Booking with index:", index, { createdBooking });
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
recurringEventId: mockBookingData.recurringEventId,
references: [
{
type: "daily_video",
uid: "MOCK_ID",
meetingId: "MOCK_ID",
meetingPassword: "MOCK_PASS",
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
type: "google_calendar",
uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
meetingPassword: "MOCK_PASSWORD",
meetingUrl: "https://UNUSED_URL",
},
],
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
organizer,
location: "integrations:daily",
subscriberUrl: "http://my-webhook.example.com",
//FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed.
videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`,
});
}
expectWorkflowToBeTriggered();
expectSuccessfulBookingCreationEmails({
booking: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBookings[0].uid!,
urlOrigin: WEBAPP_URL,
},
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
recurrence: {
...recurrence,
count: recurringCountInRequest,
},
});
expect(emails.get().length).toBe(2);
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, [
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
{
calendarId: "event-type-1@google-calendar.com",
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
},
]);
},
timeout
);
});
function getRecurrence({
type,
numberOfOccurrences,
}: {
type: "weekly" | "monthly" | "yearly";
numberOfOccurrences: number;
}) {
const freq = type === "yearly" ? 0 : type === "monthly" ? 1 : 2;
return { freq, count: numberOfOccurrences, interval: 1 };
}
});
@@ -128,7 +128,6 @@ const DestinationCalendarSelector = ({
return (
<div className="relative" title={`${t("create_events_on")}: ${selectedOption?.label || ""}`}>
<p className="text-sm font-medium leading-none">{t("add_events_to")}</p>
<Select
name="primarySelectedCalendar"
placeholder={
@@ -64,16 +64,19 @@ const getReturnUrl = (props: Props) => {
};
const PaymentForm = (props: Props) => {
const {
user: { username },
} = props;
const { t, i18n } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const [state, setState] = useState<States>({ status: "idle" });
const [isCanceling, setIsCanceling] = useState<boolean>(false);
const stripe = useStripe();
const elements = useElements();
const paymentOption = props.payment.paymentOption;
const [holdAcknowledged, setHoldAcknowledged] = useState<boolean>(paymentOption === "HOLD" ? false : true);
const bookingSuccessRedirect = useBookingSuccessRedirect();
useEffect(() => {
elements?.update({ locale: i18n.language as StripeElementLocale });
}, [elements, i18n.language]);
@@ -134,6 +137,8 @@ const PaymentForm = (props: Props) => {
}
};
const disableButtons = isCanceling || !holdAcknowledged || ["processing", "error"].includes(state.status);
return (
<form id="payment-form" className="bg-subtle mt-4 rounded-md p-6" onSubmit={handleSubmit}>
<div>
@@ -154,14 +159,22 @@ const PaymentForm = (props: Props) => {
<div className="mt-2 flex justify-end space-x-2">
<Button
color="minimal"
disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)}
disabled={disableButtons}
id="cancel"
onClick={() => router.back()}>
type="button"
loading={isCanceling}
onClick={() => {
setIsCanceling(true);
if (username) {
return router.push(`/${username}`);
}
return router.back();
}}>
<span id="button-text">{t("cancel")}</span>
</Button>
<Button
type="submit"
disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)}
disabled={disableButtons}
loading={state.status === "processing"}
id="submit"
color="secondary">
@@ -326,7 +326,7 @@ interface IOption {
* 23:45:00 (End of day with enough time for 15 min booking)
*/
/** Begin Time Increments For Select */
const INCREMENT = 15;
const INCREMENT = Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL) || 15;
const useOptions = () => {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
@@ -14,6 +14,7 @@ import logger from "../logger";
const log = logger.getSubLogger({ prefix: ["[handlePaymentSuccess]"] });
export async function handlePaymentSuccess(paymentId: number, bookingId: number) {
log.debug(`handling payment success for bookingId ${bookingId}`);
const { booking, user: userWithCredentials, evt, eventType } = await getBooking(bookingId);
if (booking.location) evt.location = booking.location;
+43
View File
@@ -300,3 +300,46 @@ describe("Tests the slot logic", () => {
expect(slots[0].time.format()).toBe("2023-07-13T08:00:00+05:30");
});
});
describe("Tests the date-range slot logic with custom env variable", () => {
beforeAll(() => {
vi.stubEnv("NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL", "10");
});
it("can fit 11 10 minute slots within a 2 hour window using a 10 mintue availabilty option with a starting time of 10 past the hour", async () => {
expect(Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL)).toBe(10);
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 10,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: 10,
endTime: 120,
},
],
eventLength: 10,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
});
it("test buildSlotsWithDateRanges using a 10 mintue interval", async () => {
expect(Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL)).toBe(10);
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 10,
minimumBookingNotice: 0,
eventLength: 10,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
dateRanges: [{ start: dayjs("2023-07-13T00:10:00.000Z"), end: dayjs("2023-07-13T02:00:00.000Z") }],
})
).toHaveLength(11);
});
});
+1 -1
View File
@@ -169,7 +169,7 @@ function buildSlotsWithDateRanges({
? range.start
: startTimeWithMinNotice;
let interval = 15;
let interval = Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL) || 15;
const intervalsWithDefinedStartTimes = [60, 30, 20, 10];
+41 -53
View File
@@ -6,66 +6,54 @@ import { bookingReferenceMiddleware } from "./middleware";
const prismaOptions: Prisma.PrismaClientOptions = {};
const globalForPrisma = global as unknown as {
prismaWithoutClientExtensions: PrismaClientWithoutExtension;
prismaWithClientExtensions: PrismaClientWithExtensions;
};
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"];
// Prevents flooding with idle connections
const prismaWithoutClientExtensions =
globalForPrisma.prismaWithoutClientExtensions || new PrismaClientWithoutExtension(prismaOptions);
export const customPrisma = (options?: Prisma.PrismaClientOptions) =>
new PrismaClientWithoutExtension({ ...prismaOptions, ...options }).$extends(withAccelerate());
// If any changed on middleware server restart is required
// TODO: Migrate it to $extends
bookingReferenceMiddleware(prismaWithoutClientExtensions);
export const prisma = (function () {
// Prevents flooding with idle connections
const prismaWithoutClientExtensions = new PrismaClientWithoutExtension(prismaOptions);
// If any changed on middleware server restart is required
// TODO: Migrate it to $extends
bookingReferenceMiddleware(prismaWithoutClientExtensions);
// FIXME: Due to some reason, there are types failing in certain places due to the $extends. Fix it and then enable it
// Specifically we get errors like `Type 'string | Date | null | undefined' is not assignable to type 'Exact<string | Date | null | undefined, string | Date | null | undefined>'`
const prismaWithClientExtensions = prismaWithoutClientExtensions
//
.$extends(withAccelerate());
// .$extends({
// query: {
// $allModels: {
// async $allOperations({ model, operation, args, query }) {
// const start = performance.now();
// /* your custom logic here */
// const res = await query(args);
// const end = performance.now();
// logger.debug("Query Perf: ", `${model}.${operation} took ${(end - start).toFixed(2)}ms\n`);
// return res;
// },
// },
// },
// });
// .$extends({
// name: "teamUpdateWithMetadata",
// query: {
// team: {
// async update({ model, operation, args, query }) {
// if (args.data.metadata) {
// // Prepare args.data with merged metadata
// }
// return query(args);
// },
// },
// },
// })
// FIXME: Due to some reason, there are types failing in certain places due to the $extends. Fix it and then enable it
// Specifically we get errors like `Type 'string | Date | null | undefined' is not assignable to type 'Exact<string | Date | null | undefined, string | Date | null | undefined>'`
const prismaWithClientExtensions = prismaWithoutClientExtensions
//
.$extends(withAccelerate());
// .$extends({
// query: {
// $allModels: {
// async $allOperations({ model, operation, args, query }) {
// const start = performance.now();
// /* your custom logic here */
// const res = await query(args);
// const end = performance.now();
// logger.debug("Query Perf: ", `${model}.${operation} took ${(end - start).toFixed(2)}ms\n`);
// return res;
// },
// },
// },
// });
// .$extends({
// name: "teamUpdateWithMetadata",
// query: {
// team: {
// async update({ model, operation, args, query }) {
// if (args.data.metadata) {
// // Prepare args.data with merged metadata
// }
// return query(args);
// },
// },
// },
// })
return prismaWithClientExtensions;
})();
export const prisma = globalForPrisma.prismaWithClientExtensions || prismaWithClientExtensions;
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prismaWithoutClientExtensions = prismaWithoutClientExtensions;
globalForPrisma.prismaWithClientExtensions = prisma;
}
type PrismaClientWithExtensions = typeof prismaWithClientExtensions;
export type PrismaClient = PrismaClientWithExtensions;
export type PrismaClient = typeof prisma;
export default prisma;
export * from "./selects";
@@ -109,7 +109,6 @@ export const updateHandler = async ({ input, ctx }: UpdateOptions) => {
timeZone: true,
eventType: {
select: {
_count: true,
id: true,
eventName: true,
},
+1
View File
@@ -11,6 +11,7 @@ declare namespace NodeJS {
/** @deprecated use `NEXT_PUBLIC_WEBSITE_URL` */
readonly NEXT_PUBLIC_APP_URL: string | undefined;
readonly NEXTAUTH_SECRET: string | undefined;
readonly NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL: string | undefined;
readonly MS_GRAPH_CLIENT_ID: string | undefined;
readonly MS_GRAPH_CLIENT_SECRET: string | undefined;
readonly ZOOM_CLIENT_ID: string | undefined;
+2
View File
@@ -25,6 +25,7 @@
"dependsOn": ["^build"],
"outputs": [".next/**"],
"env": [
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL",
"NEXT_PUBLIC_IS_E2E",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY",
@@ -70,6 +71,7 @@
"DATOCMS_WEBHOOK_SECRET",
"DATOCMS_PREVIEW_SECRET",
"ENVIRONMENT_URL",
"NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL",
"NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN",
"NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE",
"NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE",