Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 |
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||||
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
import { PrismaRoutingFormResponseRepository } from "@/lib/repositories/prisma-routing-form-response.repository";
|
||||||
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
import { PrismaScheduleRepository } from "@/lib/repositories/prisma-schedule.repository";
|
||||||
@@ -12,6 +13,7 @@ import { BusyTimesService } from "@/lib/services/busy-times.service";
|
|||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
@@ -31,6 +33,7 @@ import { Module } from "@nestjs/common";
|
|||||||
PrismaTeamRepository,
|
PrismaTeamRepository,
|
||||||
RedisService,
|
RedisService,
|
||||||
PrismaFeaturesRepository,
|
PrismaFeaturesRepository,
|
||||||
|
PrismaMembershipRepository,
|
||||||
CheckBookingLimitsService,
|
CheckBookingLimitsService,
|
||||||
CacheService,
|
CacheService,
|
||||||
AvailableSlotsService,
|
AvailableSlotsService,
|
||||||
@@ -38,6 +41,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BusyTimesService,
|
BusyTimesService,
|
||||||
FilterHostsService,
|
FilterHostsService,
|
||||||
QualifiedHostsService,
|
QualifiedHostsService,
|
||||||
|
NoSlotsNotificationService,
|
||||||
],
|
],
|
||||||
exports: [AvailableSlotsService],
|
exports: [AvailableSlotsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaMembershipRepository as PrismaMembershipRepositoryLib } from "@calcom/platform-libraries/repositories";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaMembershipRepository extends PrismaMembershipRepositoryLib {
|
||||||
|
constructor(private readonly dbWrite: PrismaWriteService) {
|
||||||
|
super(dbWrite.prisma as unknown as PrismaClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
|||||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||||
import { CacheService } from "@/lib/services/cache.service";
|
import { CacheService } from "@/lib/services/cache.service";
|
||||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||||
|
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
@@ -35,7 +36,8 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
checkBookingLimitsService: CheckBookingLimitsService,
|
checkBookingLimitsService: CheckBookingLimitsService,
|
||||||
cacheService: CacheService,
|
cacheService: CacheService,
|
||||||
userAvailabilityService: UserAvailabilityService,
|
userAvailabilityService: UserAvailabilityService,
|
||||||
busyTimesService: BusyTimesService
|
busyTimesService: BusyTimesService,
|
||||||
|
noSlotsNotificationService: NoSlotsNotificationService
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
oooRepo: oooRepoDependency,
|
oooRepo: oooRepoDependency,
|
||||||
@@ -53,6 +55,7 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
|||||||
busyTimesService,
|
busyTimesService,
|
||||||
qualifiedHostsService,
|
qualifiedHostsService,
|
||||||
featuresRepo: featuresRepository,
|
featuresRepo: featuresRepository,
|
||||||
|
noSlotsNotificationService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PrismaMembershipRepository } from "@/lib/repositories/prisma-membership.repository";
|
||||||
|
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||||
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { NoSlotsNotificationService as BaseNoSlotsNotificationService } from "@calcom/platform-libraries/slots";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NoSlotsNotificationService extends BaseNoSlotsNotificationService {
|
||||||
|
constructor(
|
||||||
|
teamRepository: PrismaTeamRepository,
|
||||||
|
membershipRepository: PrismaMembershipRepository,
|
||||||
|
redisService: RedisService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
teamRepo: teamRepository,
|
||||||
|
membershipRepo: membershipRepository,
|
||||||
|
redisClient: redisService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createRouterCaller } from "app/_trpc/context";
|
import { createRouterCaller } from "app/_trpc/context";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
@@ -31,6 +35,11 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
|||||||
redirect("/apps/installed/calendar");
|
redirect("/apps/installed/calendar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||||
createRouterCaller(calendarsRouter),
|
createRouterCaller(calendarsRouter),
|
||||||
createRouterCaller(appsRouter),
|
createRouterCaller(appsRouter),
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/about"
|
"/settings/organizations/new/about"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/add-teams"
|
"/settings/organizations/new/add-teams"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||||
|
|
||||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/handover"
|
"/settings/organizations/new/handover"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
+7
-12
@@ -1,21 +1,16 @@
|
|||||||
import type { GetServerSidePropsResult } from "next";
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const featuresRepository = new FeaturesRepository(prisma);
|
const featuresRepository = new FeaturesRepository(prisma);
|
||||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||||
// Check if organizations are enabled
|
|
||||||
if (!organizations) {
|
if (!organizations) {
|
||||||
return {
|
return notFound();
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return children;
|
||||||
props: {
|
}
|
||||||
isOrg: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+1
-10
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/onboard-members"
|
"/settings/organizations/new/onboard-members"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
export const generateMetadata = async () =>
|
||||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new"
|
"/settings/organizations/new"
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
const ServerPage = async () => {
|
||||||
isOrg: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
|
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
|
||||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
|
||||||
|
|
||||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||||
|
|
||||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
|||||||
"/settings/organizations/new/status"
|
"/settings/organizations/new/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
|
||||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
|
||||||
return (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<LegacyPage />
|
<LegacyPage />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.17",
|
"version": "5.5.18",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -4264,9 +4264,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||||
@@ -4328,9 +4326,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||||
@@ -4402,9 +4398,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||||
@@ -4474,9 +4468,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||||
"Orgs / Teams / Event Types / Private Links"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||||
@@ -22121,13 +22113,7 @@
|
|||||||
"example": "2025-12-31T23:59:59.000Z"
|
"example": "2025-12-31T23:59:59.000Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"expiresAt"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"UsageBasedPrivateLinkOutput": {
|
"UsageBasedPrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22164,14 +22150,7 @@
|
|||||||
"example": 3
|
"example": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||||
"linkId",
|
|
||||||
"eventTypeId",
|
|
||||||
"isExpired",
|
|
||||||
"bookingUrl",
|
|
||||||
"maxUsageCount",
|
|
||||||
"usageCount"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkOutput": {
|
"CreatePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22193,10 +22172,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"GetPrivateLinksOutput": {
|
"GetPrivateLinksOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22221,10 +22197,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"UpdatePrivateLinkOutput": {
|
"UpdatePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22246,10 +22219,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"DeletePrivateLinkOutput": {
|
"DeletePrivateLinkOutput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -22274,10 +22244,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["status", "data"]
|
||||||
"status",
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"StripConnectOutputDto": {
|
"StripConnectOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -26267,205 +26234,6 @@
|
|||||||
},
|
},
|
||||||
"required": ["status", "data"]
|
"required": ["status", "data"]
|
||||||
},
|
},
|
||||||
"CreatePrivateLinkInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for time-based links",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TimeBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Expiration date for this time-based link",
|
|
||||||
"format": "date-time",
|
|
||||||
"example": "2025-12-31T23:59:59.000Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
|
||||||
},
|
|
||||||
"UsageBasedPrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The private link ID",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"eventTypeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Event type ID this link belongs to",
|
|
||||||
"example": 123
|
|
||||||
},
|
|
||||||
"isExpired": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether the link is currently expired",
|
|
||||||
"example": false
|
|
||||||
},
|
|
||||||
"bookingUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full booking URL for this private link",
|
|
||||||
"format": "uri",
|
|
||||||
"example": "https://cal.com/d/abc123def456/30min"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Maximum number of times this link can be used",
|
|
||||||
"example": 10
|
|
||||||
},
|
|
||||||
"usageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Current usage count for this link",
|
|
||||||
"example": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
|
||||||
},
|
|
||||||
"CreatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Created private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"GetPrivateLinksOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
|
||||||
"items": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"expiresAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
|
||||||
"description": "New expiration date for time-based links",
|
|
||||||
"example": "2024-12-31T23:59:59.000Z"
|
|
||||||
},
|
|
||||||
"maxUsageCount": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "New maximum number of times the link can be used",
|
|
||||||
"example": 10,
|
|
||||||
"minimum": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UpdatePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Updated private link data (either time-based or usage-based)",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"DeletePrivateLinkOutput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Response status",
|
|
||||||
"example": "success"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Deleted link information",
|
|
||||||
"properties": {
|
|
||||||
"linkId": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "abc123def456"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "Private link deleted successfully"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["status", "data"]
|
|
||||||
},
|
|
||||||
"UserWebhookOutputDto": {
|
"UserWebhookOutputDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Below is a list of props that can be passed to the booker atom.
|
|||||||
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
| onTimeslotsLoaded | No | Callback function triggered once the available timeslots have been fetched. |
|
||||||
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
|
||||||
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
|
||||||
|
| silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled |
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
|||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaseBookingEventPayload = {
|
||||||
|
title: string | undefined;
|
||||||
|
startTime: string | undefined;
|
||||||
|
endTime: string | undefined;
|
||||||
|
eventTypeId: number | null | undefined;
|
||||||
|
status: string | undefined;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
/**
|
||||||
|
* This is only used for recurring bookings
|
||||||
|
*/
|
||||||
|
allBookings?: { startTime: string; endTime: string }[];
|
||||||
|
videoCallUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EventDataMap = {
|
export type EventDataMap = {
|
||||||
eventTypeSelected: {
|
eventTypeSelected: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -24,20 +39,8 @@ export type EventDataMap = {
|
|||||||
linkReady: Record<string, never>;
|
linkReady: Record<string, never>;
|
||||||
__connectInitiated: Record<string, never>;
|
__connectInitiated: Record<string, never>;
|
||||||
__connectCompleted: Record<string, never>;
|
__connectCompleted: Record<string, never>;
|
||||||
bookingSuccessfulV2: {
|
bookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
videoCallUrl?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,20 +59,11 @@ export type EventDataMap = {
|
|||||||
};
|
};
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
};
|
};
|
||||||
rescheduleBookingSuccessfulV2: {
|
rescheduleBookingSuccessfulV2: BaseBookingEventPayload & {
|
||||||
uid: string | undefined;
|
uid: string | undefined;
|
||||||
title: string | undefined;
|
|
||||||
startTime: string | undefined;
|
|
||||||
endTime: string | undefined;
|
|
||||||
eventTypeId: number | null | undefined;
|
|
||||||
status: string | undefined;
|
|
||||||
paymentRequired: boolean;
|
|
||||||
isRecurring: boolean;
|
|
||||||
/**
|
|
||||||
* This is only used for recurring bookings
|
|
||||||
*/
|
|
||||||
allBookings?: { startTime: string; endTime: string }[];
|
|
||||||
};
|
};
|
||||||
|
dryRunBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
|
dryRunRescheduleBookingSuccessfulV2: BaseBookingEventPayload;
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
* @deprecated Use `rescheduleBookingSuccessfulV2` instead. We restrict the data heavily there, only sending what is absolutely needed and keeping it light as well. Plus, more importantly that can be documented well.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -57,6 +57,28 @@ export interface IUseBookings {
|
|||||||
isBookingDryRun?: boolean;
|
isBookingDryRun?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBaseBookingEventPayload = (booking: {
|
||||||
|
title?: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
status?: BookingStatus;
|
||||||
|
paymentRequired: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
videoCallUrl?: string;
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
title: booking.title,
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
eventTypeId: booking.eventTypeId,
|
||||||
|
status: booking.status,
|
||||||
|
paymentRequired: booking.paymentRequired,
|
||||||
|
isRecurring: booking.isRecurring,
|
||||||
|
videoCallUrl: booking.videoCallUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getBookingSuccessfulEventPayload = (booking: {
|
const getBookingSuccessfulEventPayload = (booking: {
|
||||||
title?: string;
|
title?: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -70,18 +92,15 @@ const getBookingSuccessfulEventPayload = (booking: {
|
|||||||
}) => {
|
}) => {
|
||||||
return {
|
return {
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
title: booking.title,
|
...getBaseBookingEventPayload(booking),
|
||||||
startTime: booking.startTime,
|
|
||||||
endTime: booking.endTime,
|
|
||||||
eventTypeId: booking.eventTypeId,
|
|
||||||
status: booking.status,
|
|
||||||
paymentRequired: booking.paymentRequired,
|
|
||||||
isRecurring: booking.isRecurring,
|
|
||||||
videoCallUrl: booking.videoCallUrl,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
const getRescheduleBookingSuccessfulEventPayload = getBookingSuccessfulEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunBookingSuccessfulEventPayload = getBaseBookingEventPayload;
|
||||||
|
|
||||||
|
export const getDryRunRescheduleBookingSuccessfulEventPayload = getDryRunBookingSuccessfulEventPayload;
|
||||||
export interface IUseBookingLoadingStates {
|
export interface IUseBookingLoadingStates {
|
||||||
creatingBooking: boolean;
|
creatingBooking: boolean;
|
||||||
creatingRecurringBooking: boolean;
|
creatingRecurringBooking: boolean;
|
||||||
@@ -196,6 +215,30 @@ export const useBookings = ({
|
|||||||
mutationFn: createBooking,
|
mutationFn: createBooking,
|
||||||
onSuccess: (booking) => {
|
onSuccess: (booking) => {
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
const validDuration = event.data?.isDynamic
|
||||||
|
? duration || event.data?.length
|
||||||
|
: duration && event.data?.metadata?.multipleDuration?.includes(duration)
|
||||||
|
? duration
|
||||||
|
: event.data?.length;
|
||||||
|
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunRescheduleBookingSuccessfulV2",
|
||||||
|
getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire(
|
||||||
|
"dryRunBookingSuccessfulV2",
|
||||||
|
getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -343,6 +386,30 @@ export const useBookings = ({
|
|||||||
const booking = bookings[0] || {};
|
const booking = bookings[0] || {};
|
||||||
|
|
||||||
if (booking.isDryRun) {
|
if (booking.isDryRun) {
|
||||||
|
if (isRescheduling) {
|
||||||
|
sdkActionManager?.fire("dryRunRescheduleBookingSuccessfulV2", {
|
||||||
|
...getDryRunRescheduleBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sdkActionManager?.fire("dryRunBookingSuccessfulV2", {
|
||||||
|
...getDryRunBookingSuccessfulEventPayload({
|
||||||
|
...booking,
|
||||||
|
isRecurring: true,
|
||||||
|
}),
|
||||||
|
allBookings: bookings.map((booking) => ({
|
||||||
|
startTime: booking.startTime,
|
||||||
|
endTime: booking.endTime,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.push("/booking/dry-run-successful");
|
router.push("/booking/dry-run-successful");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1430,6 +1430,8 @@ async function handler(
|
|||||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
||||||
originalRescheduledBooking.userId !== evt.organizer.id;
|
originalRescheduledBooking.userId !== evt.organizer.id;
|
||||||
|
|
||||||
|
const skipDeleteEventsAndMeetings = changedOrganizer;
|
||||||
|
|
||||||
const isBookingRequestedReschedule =
|
const isBookingRequestedReschedule =
|
||||||
!!originalRescheduledBooking &&
|
!!originalRescheduledBooking &&
|
||||||
!!originalRescheduledBooking.rescheduled &&
|
!!originalRescheduledBooking.rescheduled &&
|
||||||
@@ -1702,7 +1704,8 @@ async function handler(
|
|||||||
undefined,
|
undefined,
|
||||||
changedOrganizer,
|
changedOrganizer,
|
||||||
previousHostDestinationCalendar,
|
previousHostDestinationCalendar,
|
||||||
isBookingRequestedReschedule
|
isBookingRequestedReschedule,
|
||||||
|
skipDeleteEventsAndMeetings
|
||||||
);
|
);
|
||||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||||
// to the default description when we are sending the emails.
|
// to the default description when we are sending the emails.
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* Tests for calendar event deletion during round robin organizer change operations.
|
||||||
|
* Covers scenarios where events need to be deleted from original hosts and
|
||||||
|
* credential handling during organizer changes.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getDate,
|
||||||
|
createBookingScenario,
|
||||||
|
getScenarioData,
|
||||||
|
getMockBookingAttendee,
|
||||||
|
TestData,
|
||||||
|
getOrganizer,
|
||||||
|
getGoogleCalendarCredential,
|
||||||
|
mockCalendarToHaveNoBusySlots,
|
||||||
|
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||||
|
import {
|
||||||
|
expectBookingToBeInDatabase,
|
||||||
|
expectSuccessfulRoundRobinReschedulingEmails,
|
||||||
|
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||||
|
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||||
|
|
||||||
|
import { describe, expect } from "vitest";
|
||||||
|
|
||||||
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||||
|
import { SchedulingType, BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||||
|
|
||||||
|
describe("roundRobinReassignment test", () => {
|
||||||
|
setupAndTeardown();
|
||||||
|
|
||||||
|
test("should delete calendar events from original host when round robin reassignment changes organizer", async ({
|
||||||
|
emails,
|
||||||
|
}) => {
|
||||||
|
const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
|
||||||
|
|
||||||
|
const originalHost = getOrganizer({
|
||||||
|
name: "Original Host",
|
||||||
|
email: "originalhost@example.com",
|
||||||
|
id: 101,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newHost = getOrganizer({
|
||||||
|
name: "New Host",
|
||||||
|
email: "newhost@example.com",
|
||||||
|
id: 102,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
|
||||||
|
const bookingToReassignUid = "booking-to-reassign-with-deletion";
|
||||||
|
|
||||||
|
await createBookingScenario(
|
||||||
|
getScenarioData({
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slug: "round-robin-event",
|
||||||
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||||
|
length: 45,
|
||||||
|
users: [{ id: 101 }, { id: 102 }],
|
||||||
|
hosts: [
|
||||||
|
{ userId: 101, isFixed: false },
|
||||||
|
{ userId: 102, isFixed: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bookings: [
|
||||||
|
{
|
||||||
|
id: 123,
|
||||||
|
eventTypeId: 1,
|
||||||
|
userId: originalHost.id,
|
||||||
|
uid: bookingToReassignUid,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
startTime: `${dateStringPlusOne}T05:00:00.000Z`,
|
||||||
|
endTime: `${dateStringPlusOne}T05:45:00.000Z`,
|
||||||
|
attendees: [
|
||||||
|
getMockBookingAttendee({
|
||||||
|
id: 2,
|
||||||
|
name: "attendee",
|
||||||
|
email: "attendee@test.com",
|
||||||
|
locale: "en",
|
||||||
|
timeZone: "Asia/Kolkata",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: appStoreMetadata.googlecalendar.type,
|
||||||
|
uid: "ORIGINAL_EVENT_ID",
|
||||||
|
meetingId: "ORIGINAL_EVENT_ID",
|
||||||
|
meetingPassword: null,
|
||||||
|
meetingUrl: null,
|
||||||
|
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||||
|
credentialId: 1,
|
||||||
|
deleted: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizer: originalHost,
|
||||||
|
usersApartFromOrganizer: [newHost],
|
||||||
|
apps: [TestData.apps["google-calendar"]],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||||
|
create: { uid: "NEW_EVENT_ID" },
|
||||||
|
update: { uid: "UPDATED_EVENT_ID" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await roundRobinReassignment({
|
||||||
|
bookingId: 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that calendar deletion occurred (may be called multiple times due to duplicate references)
|
||||||
|
expect(calendarMock.deleteEventCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const deleteCall = calendarMock.deleteEventCalls[0];
|
||||||
|
expect(deleteCall.args.uid).toBe("ORIGINAL_EVENT_ID");
|
||||||
|
expect(deleteCall.args.externalCalendarId).toBe("MOCK_EXTERNAL_CALENDAR_ID");
|
||||||
|
expect(deleteCall.args.event.organizer.email).toBe(newHost.email); // Current implementation uses new host credentials
|
||||||
|
expect(deleteCall.args.event.uid).toBe(bookingToReassignUid);
|
||||||
|
|
||||||
|
// Verify that creation occurred with new host credentials
|
||||||
|
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||||
|
const createCall = calendarMock.createEventCalls[0];
|
||||||
|
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
|
||||||
|
|
||||||
|
// Verify the booking was reassigned to the new host
|
||||||
|
expectBookingToBeInDatabase({
|
||||||
|
uid: bookingToReassignUid,
|
||||||
|
userId: newHost.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSuccessfulRoundRobinReschedulingEmails({
|
||||||
|
prevOrganizer: originalHost,
|
||||||
|
newOrganizer: newHost,
|
||||||
|
emails,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -570,7 +570,8 @@ export default class EventManager {
|
|||||||
newBookingId?: number,
|
newBookingId?: number,
|
||||||
changedOrganizer?: boolean,
|
changedOrganizer?: boolean,
|
||||||
previousHostDestinationCalendar?: DestinationCalendar[] | null,
|
previousHostDestinationCalendar?: DestinationCalendar[] | null,
|
||||||
isBookingRequestedReschedule?: boolean
|
isBookingRequestedReschedule?: boolean,
|
||||||
|
skipDeleteEventsAndMeetings?: boolean
|
||||||
): Promise<CreateUpdateResult> {
|
): Promise<CreateUpdateResult> {
|
||||||
const originalEvt = processLocation(event);
|
const originalEvt = processLocation(event);
|
||||||
const evt = cloneDeep(originalEvt);
|
const evt = cloneDeep(originalEvt);
|
||||||
@@ -637,7 +638,7 @@ export default class EventManager {
|
|||||||
const shouldUpdateBookingReferences =
|
const shouldUpdateBookingReferences =
|
||||||
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
||||||
|
|
||||||
if (evt.requiresConfirmation && !changedOrganizer) {
|
if (evt.requiresConfirmation && !skipDeleteEventsAndMeetings) {
|
||||||
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
||||||
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
|
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
|
||||||
await this.deleteEventsAndMeetings({
|
await this.deleteEventsAndMeetings({
|
||||||
@@ -646,6 +647,14 @@ export default class EventManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (changedOrganizer) {
|
if (changedOrganizer) {
|
||||||
|
if (!skipDeleteEventsAndMeetings) {
|
||||||
|
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
|
||||||
|
await this.deleteEventsAndMeetings({
|
||||||
|
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
|
||||||
|
bookingReferences: booking.references,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
||||||
const createdEvent = await this.create(originalEvt);
|
const createdEvent = await this.create(originalEvt);
|
||||||
results.push(...createdEvent.results);
|
results.push(...createdEvent.results);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { eventTypeRepositoryModule } from "../modules/EventType";
|
|||||||
import { featuresRepositoryModule } from "../modules/Features";
|
import { featuresRepositoryModule } from "../modules/Features";
|
||||||
import { filterHostsModule } from "../modules/FilterHosts";
|
import { filterHostsModule } from "../modules/FilterHosts";
|
||||||
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
|
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
|
||||||
|
import { membershipRepositoryModule } from "../modules/Membership";
|
||||||
|
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
|
||||||
import { oooRepositoryModule } from "../modules/Ooo";
|
import { oooRepositoryModule } from "../modules/Ooo";
|
||||||
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
|
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
|
||||||
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
|
import { routingFormResponseRepositoryModule } from "../modules/RoutingFormResponse";
|
||||||
@@ -29,6 +31,7 @@ container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
|
|||||||
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
container.load(DI_TOKENS.SCHEDULE_REPOSITORY_MODULE, scheduleRepositoryModule);
|
||||||
container.load(DI_TOKENS.SELECTED_SLOT_REPOSITORY_MODULE, selectedSlotsRepositoryModule);
|
container.load(DI_TOKENS.SELECTED_SLOT_REPOSITORY_MODULE, selectedSlotsRepositoryModule);
|
||||||
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
|
||||||
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
|
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
|
||||||
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
|
||||||
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
|
||||||
@@ -42,6 +45,7 @@ container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
|||||||
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
|
||||||
container.load(DI_TOKENS.FILTER_HOSTS_SERVICE_MODULE, filterHostsModule);
|
container.load(DI_TOKENS.FILTER_HOSTS_SERVICE_MODULE, filterHostsModule);
|
||||||
container.load(DI_TOKENS.QUALIFIED_HOSTS_SERVICE_MODULE, qualifiedHostsModule);
|
container.load(DI_TOKENS.QUALIFIED_HOSTS_SERVICE_MODULE, qualifiedHostsModule);
|
||||||
|
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
|
||||||
|
|
||||||
export function getAvailableSlotsService() {
|
export function getAvailableSlotsService() {
|
||||||
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
|
return container.get<AvailableSlotsService>(DI_TOKENS.AVAILABLE_SLOTS_SERVICE);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createContainer } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { redisModule } from "@calcom/features/redis/di/redisModule";
|
||||||
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
|
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||||
|
import type { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
|
||||||
|
|
||||||
|
import { membershipRepositoryModule } from "../modules/Membership";
|
||||||
|
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
|
||||||
|
import { teamRepositoryModule } from "../modules/Team";
|
||||||
|
|
||||||
|
const container = createContainer();
|
||||||
|
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
|
||||||
|
container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
|
||||||
|
container.load(DI_TOKENS.TEAM_REPOSITORY_MODULE, teamRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModule);
|
||||||
|
container.load(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE_MODULE, noSlotsNotificationModule);
|
||||||
|
|
||||||
|
export function getNoSlotsNotificationService() {
|
||||||
|
return container.get<NoSlotsNotificationService>(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE);
|
||||||
|
}
|
||||||
@@ -22,4 +22,5 @@ availableSlotsModule.bind(DI_TOKENS.AVAILABLE_SLOTS_SERVICE).toClass(AvailableSl
|
|||||||
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
|
busyTimesService: DI_TOKENS.BUSY_TIMES_SERVICE,
|
||||||
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
|
featuresRepo: DI_TOKENS.FEATURES_REPOSITORY,
|
||||||
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
|
qualifiedHostsService: DI_TOKENS.QUALIFIED_HOSTS_SERVICE,
|
||||||
|
noSlotsNotificationService: DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE,
|
||||||
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
} satisfies Record<keyof IAvailableSlotsService, symbol>);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import { DI_TOKENS } from "@calcom/lib/di/tokens";
|
||||||
|
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||||
|
|
||||||
|
export const membershipRepositoryModule = createModule();
|
||||||
|
membershipRepositoryModule
|
||||||
|
.bind(DI_TOKENS.MEMBERSHIP_REPOSITORY)
|
||||||
|
.toClass(MembershipRepository, [DI_TOKENS.PRISMA_CLIENT]);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
|
import type { INoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
|
||||||
|
import { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
|
||||||
|
|
||||||
|
import { DI_TOKENS } from "../tokens";
|
||||||
|
|
||||||
|
export const noSlotsNotificationModule = createModule();
|
||||||
|
noSlotsNotificationModule.bind(DI_TOKENS.NO_SLOTS_NOTIFICATION_SERVICE).toClass(NoSlotsNotificationService, {
|
||||||
|
teamRepo: DI_TOKENS.TEAM_REPOSITORY,
|
||||||
|
membershipRepo: DI_TOKENS.MEMBERSHIP_REPOSITORY,
|
||||||
|
redisClient: DI_TOKENS.REDIS_CLIENT,
|
||||||
|
} satisfies Record<keyof INoSlotsNotificationService, symbol>);
|
||||||
@@ -41,4 +41,8 @@ export const DI_TOKENS = {
|
|||||||
QUALIFIED_HOSTS_SERVICE_MODULE: Symbol("QualifiedHostsServiceModule"),
|
QUALIFIED_HOSTS_SERVICE_MODULE: Symbol("QualifiedHostsServiceModule"),
|
||||||
FILTER_HOSTS_SERVICE: Symbol("FilterHostsService"),
|
FILTER_HOSTS_SERVICE: Symbol("FilterHostsService"),
|
||||||
FILTER_HOSTS_SERVICE_MODULE: Symbol("FilterHostsServiceModule"),
|
FILTER_HOSTS_SERVICE_MODULE: Symbol("FilterHostsServiceModule"),
|
||||||
|
NO_SLOTS_NOTIFICATION_SERVICE: Symbol("NoSlotsNotificationService"),
|
||||||
|
NO_SLOTS_NOTIFICATION_SERVICE_MODULE: Symbol("NoSlotsNotificationServiceModule"),
|
||||||
|
MEMBERSHIP_REPOSITORY: Symbol("MembershipRepository"),
|
||||||
|
MEMBERSHIP_REPOSITORY_MODULE: Symbol("MembershipRepositoryModule"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export class BusyTimesService {
|
|||||||
})[]
|
})[]
|
||||||
| null;
|
| null;
|
||||||
bypassBusyCalendarTimes: boolean;
|
bypassBusyCalendarTimes: boolean;
|
||||||
|
silentlyHandleCalendarFailures?: boolean;
|
||||||
shouldServeCache?: boolean;
|
shouldServeCache?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
@@ -70,6 +71,7 @@ export class BusyTimesService {
|
|||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
duration,
|
duration,
|
||||||
bypassBusyCalendarTimes = false,
|
bypassBusyCalendarTimes = false,
|
||||||
|
silentlyHandleCalendarFailures = false,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@@ -198,63 +200,71 @@ export class BusyTimesService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!calendarBusyTimesQuery.success) {
|
if (!calendarBusyTimesQuery.success) {
|
||||||
throw new Error(
|
if (silentlyHandleCalendarFailures) {
|
||||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
logger.warn(
|
||||||
(calendar) => calendar.id
|
`Calendar busy times fetch failed but handling silently due to silentlyHandleCalendarFailures flag for user ${username}`,
|
||||||
)}`
|
{
|
||||||
|
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||||
|
(calendar) => calendar.id
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||||
|
const endConnectedCalendarsGet = performance.now();
|
||||||
|
logger.debug(
|
||||||
|
`Connected Calendars get took ${
|
||||||
|
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||||
|
} ms for user ${username}`,
|
||||||
|
JSON.stringify({
|
||||||
|
eventTypeId,
|
||||||
|
startTimeDate,
|
||||||
|
endTimeDate,
|
||||||
|
calendarBusyTimes,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
||||||
|
const [start, end] = key.split("<>");
|
||||||
|
return {
|
||||||
|
start: dayjs(start),
|
||||||
|
end: dayjs(end),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rescheduleUid) {
|
||||||
|
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||||
|
if (originalRescheduleBooking) {
|
||||||
|
openSeatsDateRanges.push({
|
||||||
|
start: dayjs(originalRescheduleBooking.startTime),
|
||||||
|
end: dayjs(originalRescheduleBooking.endTime),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = subtract(
|
||||||
|
calendarBusyTimes.map((value) => ({
|
||||||
|
...value,
|
||||||
|
end: dayjs(value.end),
|
||||||
|
start: dayjs(value.start),
|
||||||
|
})),
|
||||||
|
openSeatsDateRanges
|
||||||
|
);
|
||||||
|
|
||||||
|
busyTimes.push(
|
||||||
|
...result.map((busyTime) => ({
|
||||||
|
...busyTime,
|
||||||
|
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
||||||
|
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
|
||||||
const endConnectedCalendarsGet = performance.now();
|
|
||||||
logger.debug(
|
|
||||||
`Connected Calendars get took ${
|
|
||||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
|
||||||
} ms for user ${username}`,
|
|
||||||
JSON.stringify({
|
|
||||||
eventTypeId,
|
|
||||||
startTimeDate,
|
|
||||||
endTimeDate,
|
|
||||||
calendarBusyTimes,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
|
||||||
const [start, end] = key.split("<>");
|
|
||||||
return {
|
|
||||||
start: dayjs(start),
|
|
||||||
end: dayjs(end),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rescheduleUid) {
|
|
||||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
|
||||||
// calendar busy time from original rescheduled booking should not be blocked
|
|
||||||
if (originalRescheduleBooking) {
|
|
||||||
openSeatsDateRanges.push({
|
|
||||||
start: dayjs(originalRescheduleBooking.startTime),
|
|
||||||
end: dayjs(originalRescheduleBooking.endTime),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = subtract(
|
|
||||||
calendarBusyTimes.map((value) => ({
|
|
||||||
...value,
|
|
||||||
end: dayjs(value.end),
|
|
||||||
start: dayjs(value.start),
|
|
||||||
})),
|
|
||||||
openSeatsDateRanges
|
|
||||||
);
|
|
||||||
|
|
||||||
busyTimes.push(
|
|
||||||
...result.map((busyTime) => ({
|
|
||||||
...busyTime,
|
|
||||||
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
|
||||||
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// TODO: Disabled until we can filter Zoom events by date. Also this is adding too much latency.
|
// TODO: Disabled until we can filter Zoom events by date. Also this is adding too much latency.
|
||||||
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const availabilitySchema = z
|
|||||||
withSource: z.boolean().optional(),
|
withSource: z.boolean().optional(),
|
||||||
returnDateOverrides: z.boolean(),
|
returnDateOverrides: z.boolean(),
|
||||||
bypassBusyCalendarTimes: z.boolean().optional(),
|
bypassBusyCalendarTimes: z.boolean().optional(),
|
||||||
|
silentlyHandleCalendarFailures: z.boolean().optional(),
|
||||||
shouldServeCache: z.boolean().optional(),
|
shouldServeCache: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||||
@@ -110,6 +111,7 @@ type GetUserAvailabilityQuery = {
|
|||||||
duration?: number;
|
duration?: number;
|
||||||
returnDateOverrides: boolean;
|
returnDateOverrides: boolean;
|
||||||
bypassBusyCalendarTimes: boolean;
|
bypassBusyCalendarTimes: boolean;
|
||||||
|
silentlyHandleCalendarFailures?: boolean;
|
||||||
shouldServeCache?: boolean;
|
shouldServeCache?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,6 +297,7 @@ export class UserAvailabilityService {
|
|||||||
duration,
|
duration,
|
||||||
returnDateOverrides,
|
returnDateOverrides,
|
||||||
bypassBusyCalendarTimes = false,
|
bypassBusyCalendarTimes = false,
|
||||||
|
silentlyHandleCalendarFailures = false,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
} = availabilitySchema.parse(query);
|
} = availabilitySchema.parse(query);
|
||||||
|
|
||||||
@@ -457,6 +460,7 @@ export class UserAvailabilityService {
|
|||||||
duration,
|
duration,
|
||||||
currentBookings: initialData?.currentBookings,
|
currentBookings: initialData?.currentBookings,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentlyHandleCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -460,4 +460,28 @@ export class MembershipRepository {
|
|||||||
select: select ?? { userId: true },
|
select: select ?? { userId: true },
|
||||||
})) as unknown as Promise<MembershipDTOFromSelect<TSelect>[]>;
|
})) as unknown as Promise<MembershipDTOFromSelect<TSelect>[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findTeamAdminsByTeamId({ teamId }: { teamId: number }) {
|
||||||
|
return await this.prismaClient.membership.findMany({
|
||||||
|
where: {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
parentId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
in: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
locale: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,4 +350,31 @@ export class TeamRepository {
|
|||||||
|
|
||||||
return team?.parent;
|
return team?.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOrganizationSettingsBySlug({ slug }: { slug: string }) {
|
||||||
|
return await this.prismaClient.team.findFirst({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
isOrganization: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
organizationSettings: {
|
||||||
|
select: {
|
||||||
|
adminGetsNoSlotsNotification: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTeamSlugById({ id }: { id: number }) {
|
||||||
|
return await this.prismaClient.team.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
## 1.1.2
|
## 1.1.2
|
||||||
|
|
||||||
|
## 1.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#23119](https://github.com/calcom/cal.com/pull/23119) [`eb1ed10`](https://github.com/calcom/cal.com/commit/eb1ed107ba54a4ae99d0b3aa741ae3076d0ec629) Thanks [@ThyMinimalDev](https://github.com/ThyMinimalDev)! - Added Booker atom prop silentlyHandleCalendarFailures, which ensure the booker still displays slots when the third party calendars credentials are invalid or expired
|
||||||
|
|
||||||
## 1.3.1
|
## 1.3.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const BookerPlatformWrapperComponent = (
|
|||||||
onTimeslotsLoaded,
|
onTimeslotsLoaded,
|
||||||
startTime: customStartTime,
|
startTime: customStartTime,
|
||||||
showNoAvailabilityDialog,
|
showNoAvailabilityDialog,
|
||||||
|
silentlyHandleCalendarFailures = false,
|
||||||
} = props;
|
} = props;
|
||||||
const layout = BookerLayouts[view];
|
const layout = BookerLayouts[view];
|
||||||
|
|
||||||
@@ -310,6 +311,7 @@ const BookerPlatformWrapperComponent = (
|
|||||||
Boolean(event?.data?.id),
|
Boolean(event?.data?.id),
|
||||||
orgSlug: props.entity?.orgSlug ?? undefined,
|
orgSlug: props.entity?.orgSlug ?? undefined,
|
||||||
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
|
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
|
||||||
|
_silentCalendarFailures: silentlyHandleCalendarFailures,
|
||||||
...routingParams,
|
...routingParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
|
|||||||
onTimeslotsLoaded?: (slots: Record<string, Slot[]>) => void;
|
onTimeslotsLoaded?: (slots: Record<string, Slot[]>) => void;
|
||||||
startTime?: string | Date;
|
startTime?: string | Date;
|
||||||
roundRobinHideOrgAndTeam?: boolean;
|
roundRobinHideOrgAndTeam?: boolean;
|
||||||
|
silentlyHandleCalendarFailures?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type VIEW_TYPE = keyof typeof BookerLayouts;
|
type VIEW_TYPE = keyof typeof BookerLayouts;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||||
"authors": "Cal.com, Inc.",
|
"authors": "Cal.com, Inc.",
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
||||||
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
|
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
|||||||
: { username: props.calUsername })}
|
: { username: props.calUsername })}
|
||||||
hostsLimit={3}
|
hostsLimit={3}
|
||||||
allowUpdatingUrlParams={true}
|
allowUpdatingUrlParams={true}
|
||||||
|
silentlyHandleCalendarFailures={false}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { PrismaSelectedSlotRepository } from "@calcom/lib/server/repository/Pris
|
|||||||
export { TeamRepository as PrismaTeamRepository } from "@calcom/lib/server/repository/team";
|
export { TeamRepository as PrismaTeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
export { UserRepository as PrismaUserRepository } from "@calcom/lib/server/repository/user";
|
export { UserRepository as PrismaUserRepository } from "@calcom/lib/server/repository/user";
|
||||||
export { FeaturesRepository as PrismaFeaturesRepository } from "@calcom/features/flags/features.repository";
|
export { FeaturesRepository as PrismaFeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
|
export { MembershipRepository as PrismaMembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FilterHostsService } from "@calcom/lib/bookings/filterHostsBySameRoundRobinHost";
|
import { FilterHostsService } from "@calcom/lib/bookings/filterHostsBySameRoundRobinHost";
|
||||||
import { QualifiedHostsService } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
|
import { QualifiedHostsService } from "@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials";
|
||||||
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
|
import { BusyTimesService } from "@calcom/lib/getBusyTimes";
|
||||||
|
import { NoSlotsNotificationService } from "@calcom/trpc/server/routers/viewer/slots/handleNotificationWhenNoSlots";
|
||||||
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
|
import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||||
|
|
||||||
export { AvailableSlotsService };
|
export { AvailableSlotsService };
|
||||||
@@ -10,3 +11,4 @@ export { BusyTimesService };
|
|||||||
export { QualifiedHostsService };
|
export { QualifiedHostsService };
|
||||||
|
|
||||||
export { FilterHostsService };
|
export { FilterHostsService };
|
||||||
|
export { NoSlotsNotificationService };
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ export class GetAvailableSlotsInput_2024_04_15 {
|
|||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
_bypassCalendarBusyTimes?: boolean;
|
_bypassCalendarBusyTimes?: boolean;
|
||||||
|
|
||||||
|
@Transform(({ value }) => value && value.toLowerCase() === "true")
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiHideProperty()
|
||||||
|
_silentCalendarFailures?: boolean;
|
||||||
|
|
||||||
@Transform(({ value }) => (value ? value.toLowerCase() === "true" : false))
|
@Transform(({ value }) => (value ? value.toLowerCase() === "true" : false))
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
|||||||
|
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||||
|
|
||||||
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import { duplicateHandler } from "./duplicate.handler";
|
||||||
|
|
||||||
|
vi.mock("@calcom/prisma", () => ({
|
||||||
|
default: prismaMock,
|
||||||
|
}));
|
||||||
|
vi.mock("@calcom/lib/server/repository/eventTypeRepository");
|
||||||
|
|
||||||
|
describe("duplicateHandler", () => {
|
||||||
|
const ctx = { user: { id: 1, profile: { id: 1 } } } as any;
|
||||||
|
const input = { id: 123, slug: "test-event", title: "Test", description: "Test", length: 30, teamId: null };
|
||||||
|
const eventType = { id: 123, userId: 1, teamId: null, users: [{ id: 1 }] };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
prismaMock.eventType.findUnique.mockResolvedValue(eventType);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw BAD_REQUEST in case of unique constraint violation", async () => {
|
||||||
|
const { EventTypeRepository } = await import("@calcom/lib/server/repository/eventTypeRepository");
|
||||||
|
vi.mocked(EventTypeRepository).mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
create: vi.fn().mockRejectedValue(
|
||||||
|
new PrismaClientKnownRequestError("Unique constraint failed", {
|
||||||
|
code: "P2002",
|
||||||
|
clientVersion: "mockedVersion",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(duplicateHandler({ ctx, input })).rejects.toThrow(
|
||||||
|
new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Unique constraint violation while creating a duplicate event.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -226,6 +226,13 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
|
|||||||
eventType: newEventType,
|
eventType: newEventType,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||||
|
// unique constraint violation
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Unique constraint violation while creating a duplicate event.",
|
||||||
|
});
|
||||||
|
}
|
||||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error duplicating event type ${error}` });
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error duplicating event type ${error}` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { vi, describe, it, beforeAll, afterAll, expect, beforeEach, afterEach }
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import * as CalcomEmails from "@calcom/emails";
|
import * as CalcomEmails from "@calcom/emails";
|
||||||
import { RedisService } from "@calcom/features/redis/RedisService";
|
import { RedisService } from "@calcom/features/redis/RedisService";
|
||||||
|
import { getNoSlotsNotificationService } from "@calcom/lib/di/containers/NoSlotsNotification";
|
||||||
import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
|
|
||||||
|
|
||||||
vi.mock("@calcom/features/redis/RedisService", () => {
|
vi.mock("@calcom/features/redis/RedisService", () => {
|
||||||
const mockedRedis = vi.fn();
|
const mockedRedis = vi.fn();
|
||||||
@@ -84,14 +83,16 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Call the function with teamId
|
// Call the function with teamId
|
||||||
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
||||||
|
|
||||||
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
|
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// Mock length to be one then recall to trigger email
|
// Mock length to be one then recall to trigger email
|
||||||
mocked.lrange.mockResolvedValueOnce([""]);
|
mocked.lrange.mockResolvedValueOnce([""]);
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
const service2 = getNoSlotsNotificationService();
|
||||||
|
await service2.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
||||||
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).toHaveBeenCalled();
|
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it("Should not send a notification if the org has them disabled", async () => {
|
it("Should not send a notification if the org has them disabled", async () => {
|
||||||
@@ -113,7 +114,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
isValidOrgDomain: true,
|
isValidOrgDomain: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({ eventDetails, orgDetails, teamId: 123 });
|
||||||
|
|
||||||
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
|
expect(CalcomEmails.sendOrganizationAdminNoSlotsNotification).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -153,7 +155,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
mocked.lrange.mockResolvedValueOnce([""]); // This will trigger email sending
|
mocked.lrange.mockResolvedValueOnce([""]); // This will trigger email sending
|
||||||
|
|
||||||
// Call with specific teamId
|
// Call with specific teamId
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123, // specific teamId
|
teamId: 123, // specific teamId
|
||||||
@@ -205,7 +208,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
|
|
||||||
mocked.lrange.mockResolvedValueOnce([""]);
|
mocked.lrange.mockResolvedValueOnce([""]);
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
// teamId intentionally omitted
|
// teamId intentionally omitted
|
||||||
@@ -233,7 +237,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
|
|
||||||
mocked.lrange.mockResolvedValueOnce([""]);
|
mocked.lrange.mockResolvedValueOnce([""]);
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -263,7 +268,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
isValidOrgDomain: true,
|
isValidOrgDomain: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -318,7 +324,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
|
|
||||||
mocked.lrange.mockResolvedValueOnce([""]);
|
mocked.lrange.mockResolvedValueOnce([""]);
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -375,7 +382,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
|
|
||||||
// First notification cycle - simulate having one previous occurrence
|
// First notification cycle - simulate having one previous occurrence
|
||||||
mocked.lrange.mockResolvedValueOnce([""]); // One previous occurrence
|
mocked.lrange.mockResolvedValueOnce([""]); // One previous occurrence
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -391,7 +399,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
// This better simulates the real Redis state after the first notification
|
// This better simulates the real Redis state after the first notification
|
||||||
mocked.lrange.mockResolvedValueOnce(["", ""]); // Two occurrences now
|
mocked.lrange.mockResolvedValueOnce(["", ""]); // Two occurrences now
|
||||||
|
|
||||||
await handleNotificationWhenNoSlots({
|
const service2 = getNoSlotsNotificationService();
|
||||||
|
await service2.handleNotificationWhenNoSlots({
|
||||||
eventDetails,
|
eventDetails,
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -436,7 +445,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
|
|
||||||
// First event type notification
|
// First event type notification
|
||||||
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for first event
|
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for first event
|
||||||
await handleNotificationWhenNoSlots({
|
const service = getNoSlotsNotificationService();
|
||||||
|
await service.handleNotificationWhenNoSlots({
|
||||||
eventDetails: { ...baseEventDetails, eventSlug: "event1" },
|
eventDetails: { ...baseEventDetails, eventSlug: "event1" },
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
@@ -451,7 +461,8 @@ describe("(Orgs) Send admin notifications when a user has no availability", () =
|
|||||||
// For the second event type, also simulate one previous occurrence
|
// For the second event type, also simulate one previous occurrence
|
||||||
// This needs to be a separate mock since it's a different key in Redis
|
// This needs to be a separate mock since it's a different key in Redis
|
||||||
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for second event
|
mocked.lrange.mockResolvedValueOnce([""]); // Simulate one previous occurrence for second event
|
||||||
await handleNotificationWhenNoSlots({
|
const service2 = getNoSlotsNotificationService();
|
||||||
|
await service2.handleNotificationWhenNoSlots({
|
||||||
eventDetails: { ...baseEventDetails, eventSlug: "event2" },
|
eventDetails: { ...baseEventDetails, eventSlug: "event2" },
|
||||||
orgDetails,
|
orgDetails,
|
||||||
teamId: 123,
|
teamId: 123,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import { sendOrganizationAdminNoSlotsNotification } from "@calcom/emails";
|
import { sendOrganizationAdminNoSlotsNotification } from "@calcom/emails";
|
||||||
import { RedisService } from "@calcom/features/redis/RedisService";
|
import type { IRedisService } from "@calcom/features/redis/IRedisService";
|
||||||
import { IS_PRODUCTION, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_PRODUCTION, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { prisma } from "@calcom/prisma";
|
import type { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||||
|
import type { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
|
|
||||||
type EventDetails = {
|
type EventDetails = {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -38,107 +39,83 @@ const constructDataHash = (eventDetails: EventDetails) => {
|
|||||||
return JSON.stringify(obj);
|
return JSON.stringify(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleNotificationWhenNoSlots = async ({
|
export interface INoSlotsNotificationService {
|
||||||
eventDetails,
|
teamRepo: TeamRepository;
|
||||||
orgDetails,
|
membershipRepo: MembershipRepository;
|
||||||
teamId,
|
redisClient: IRedisService;
|
||||||
}: {
|
}
|
||||||
eventDetails: EventDetails;
|
|
||||||
orgDetails: { currentOrgDomain: string | null };
|
|
||||||
teamId?: number;
|
|
||||||
}) => {
|
|
||||||
// Check for org
|
|
||||||
if (!orgDetails.currentOrgDomain || !teamId) return;
|
|
||||||
const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL;
|
|
||||||
if (!UPSTASH_ENV_FOUND) return;
|
|
||||||
|
|
||||||
// Check org has this setting enabled
|
export class NoSlotsNotificationService {
|
||||||
const orgSettings = await prisma.team.findFirst({
|
constructor(public readonly dependencies: INoSlotsNotificationService) {}
|
||||||
where: {
|
|
||||||
|
async handleNotificationWhenNoSlots({
|
||||||
|
eventDetails,
|
||||||
|
orgDetails,
|
||||||
|
teamId,
|
||||||
|
}: {
|
||||||
|
eventDetails: EventDetails;
|
||||||
|
orgDetails: { currentOrgDomain: string | null };
|
||||||
|
teamId?: number;
|
||||||
|
}) {
|
||||||
|
// Check for org
|
||||||
|
if (!orgDetails.currentOrgDomain || !teamId) return;
|
||||||
|
const UPSTASH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_TOKEN && process.env.UPSTASH_REDIS_REST_URL;
|
||||||
|
if (!UPSTASH_ENV_FOUND) return;
|
||||||
|
|
||||||
|
// Check org has this setting enabled
|
||||||
|
const orgSettings = await this.dependencies.teamRepo.findOrganizationSettingsBySlug({
|
||||||
slug: orgDetails.currentOrgDomain,
|
slug: orgDetails.currentOrgDomain,
|
||||||
isOrganization: true,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
organizationSettings: {
|
|
||||||
select: {
|
|
||||||
adminGetsNoSlotsNotification: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orgSettings?.organizationSettings?.adminGetsNoSlotsNotification) return;
|
|
||||||
|
|
||||||
const redis = new RedisService();
|
|
||||||
|
|
||||||
const usersUniqueKey = constructRedisKey(eventDetails, orgDetails.currentOrgDomain, teamId);
|
|
||||||
// Get only the required amount of data so the request is as small as possible
|
|
||||||
// We may need to get more data and check the startDate occurrence of this
|
|
||||||
// Not trigger email if the start months are the same
|
|
||||||
const usersExistingNoSlots =
|
|
||||||
(await redis.lrange(usersUniqueKey, 0, NO_SLOTS_COUNT_FOR_NOTIFICATION - 1)) ?? [];
|
|
||||||
await redis.lpush(usersUniqueKey, constructDataHash(eventDetails));
|
|
||||||
|
|
||||||
if (!usersExistingNoSlots.length) {
|
|
||||||
await redis.expire(usersUniqueKey, NO_SLOTS_NOTIFICATION_FREQUENCY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We add one as we know we just added one to the list - saves us re-fetching the data
|
|
||||||
if (usersExistingNoSlots.length + 1 === NO_SLOTS_COUNT_FOR_NOTIFICATION) {
|
|
||||||
// Get all team admins to send the email too
|
|
||||||
const foundAdmins = await prisma.membership.findMany({
|
|
||||||
where: {
|
|
||||||
team: {
|
|
||||||
id: teamId,
|
|
||||||
parentId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
in: ["ADMIN", "OWNER"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
locale: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const teamSlug = await prisma.team.findUnique({
|
if (!orgSettings?.organizationSettings?.adminGetsNoSlotsNotification) return;
|
||||||
where: {
|
|
||||||
id: teamId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// TODO: use new tasker as we dont want this blocking loading slots (Just out of scope for this PR)
|
|
||||||
// Tasker isn't 100% working with emails - will refactor after i have made changes to Tasker in another PR.
|
|
||||||
const emailsToSend: Array<Promise<void>> = [];
|
|
||||||
// const tasker = getTasker();
|
|
||||||
for (const admin of foundAdmins) {
|
|
||||||
const translation = await getTranslation(admin.user.locale ?? "en", "common");
|
|
||||||
|
|
||||||
const payload = {
|
const usersUniqueKey = constructRedisKey(eventDetails, orgDetails.currentOrgDomain, teamId);
|
||||||
language: translation,
|
// Get only the required amount of data so the request is as small as possible
|
||||||
to: {
|
// We may need to get more data and check the startDate occurrence of this
|
||||||
email: admin.user.email,
|
// Not trigger email if the start months are the same
|
||||||
},
|
const usersExistingNoSlots =
|
||||||
user: eventDetails.username,
|
(await this.dependencies.redisClient.lrange(usersUniqueKey, 0, NO_SLOTS_COUNT_FOR_NOTIFICATION - 1)) ??
|
||||||
slug: eventDetails.eventSlug,
|
[];
|
||||||
startTime: eventDetails.startTime.format("YYYY-MM-DD"),
|
await this.dependencies.redisClient.lpush(usersUniqueKey, constructDataHash(eventDetails));
|
||||||
endTime: eventDetails.endTime.format("YYYY-MM-DD"),
|
|
||||||
// For now navigate here - when impersonation via parameter has been pushed we will impersonate and then navigate to availability
|
|
||||||
editLink: `${WEBAPP_URL}/availability?type=team`,
|
|
||||||
teamSlug: teamSlug?.slug ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
emailsToSend.push(sendOrganizationAdminNoSlotsNotification(payload));
|
if (!usersExistingNoSlots.length) {
|
||||||
|
await this.dependencies.redisClient.expire(usersUniqueKey, NO_SLOTS_NOTIFICATION_FREQUENCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We add one as we know we just added one to the list - saves us re-fetching the data
|
||||||
|
if (usersExistingNoSlots.length + 1 === NO_SLOTS_COUNT_FOR_NOTIFICATION) {
|
||||||
|
// Get all team admins to send the email too
|
||||||
|
const foundAdmins = await this.dependencies.membershipRepo.findTeamAdminsByTeamId({
|
||||||
|
teamId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamSlug = await this.dependencies.teamRepo.findTeamSlugById({
|
||||||
|
id: teamId,
|
||||||
|
});
|
||||||
|
// TODO: use new tasker as we dont want this blocking loading slots (Just out of scope for this PR)
|
||||||
|
// Tasker isn't 100% working with emails - will refactor after i have made changes to Tasker in another PR.
|
||||||
|
const emailsToSend: Array<Promise<void>> = [];
|
||||||
|
// const tasker = getTasker();
|
||||||
|
for (const admin of foundAdmins) {
|
||||||
|
const translation = await getTranslation(admin.user.locale ?? "en", "common");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
language: translation,
|
||||||
|
to: {
|
||||||
|
email: admin.user.email,
|
||||||
|
},
|
||||||
|
user: eventDetails.username,
|
||||||
|
slug: eventDetails.eventSlug,
|
||||||
|
startTime: eventDetails.startTime.format("YYYY-MM-DD"),
|
||||||
|
endTime: eventDetails.endTime.format("YYYY-MM-DD"),
|
||||||
|
// For now navigate here - when impersonation via parameter has been pushed we will impersonate and then navigate to availability
|
||||||
|
editLink: `${WEBAPP_URL}/availability?type=team`,
|
||||||
|
teamSlug: teamSlug?.slug ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
emailsToSend.push(sendOrganizationAdminNoSlotsNotification(payload));
|
||||||
|
}
|
||||||
|
await Promise.all(emailsToSend);
|
||||||
}
|
}
|
||||||
await Promise.all(emailsToSend);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const getScheduleSchemaObject = z.object({
|
|||||||
skipContactOwner: z.boolean().nullish(),
|
skipContactOwner: z.boolean().nullish(),
|
||||||
_enableTroubleshooter: z.boolean().optional(),
|
_enableTroubleshooter: z.boolean().optional(),
|
||||||
_bypassCalendarBusyTimes: z.boolean().optional(),
|
_bypassCalendarBusyTimes: z.boolean().optional(),
|
||||||
|
_silentCalendarFailures: z.boolean().optional(),
|
||||||
_shouldServeCache: z.boolean().optional(),
|
_shouldServeCache: z.boolean().optional(),
|
||||||
routingFormResponseId: z.number().optional(),
|
routingFormResponseId: z.number().optional(),
|
||||||
queuedFormResponseId: z.string().nullish(),
|
queuedFormResponseId: z.string().nullish(),
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
||||||
import { handleNotificationWhenNoSlots } from "./handleNotificationWhenNoSlots";
|
import type { NoSlotsNotificationService } from "./handleNotificationWhenNoSlots";
|
||||||
import type { GetScheduleOptions } from "./types";
|
import type { GetScheduleOptions } from "./types";
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
|
const log = logger.getSubLogger({ prefix: ["[slots/util]"] });
|
||||||
@@ -108,6 +108,7 @@ export interface IAvailableSlotsService {
|
|||||||
redisClient: IRedisService;
|
redisClient: IRedisService;
|
||||||
featuresRepo: FeaturesRepository;
|
featuresRepo: FeaturesRepository;
|
||||||
qualifiedHostsService: QualifiedHostsService;
|
qualifiedHostsService: QualifiedHostsService;
|
||||||
|
noSlotsNotificationService: NoSlotsNotificationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withSlotsCache(
|
function withSlotsCache(
|
||||||
@@ -737,6 +738,7 @@ export class AvailableSlotsService {
|
|||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
}: {
|
}: {
|
||||||
input: TGetScheduleInputSchema;
|
input: TGetScheduleInputSchema;
|
||||||
@@ -753,6 +755,7 @@ export class AvailableSlotsService {
|
|||||||
startTime: ReturnType<(typeof AvailableSlotsService)["prototype"]["getStartTime"]>;
|
startTime: ReturnType<(typeof AvailableSlotsService)["prototype"]["getStartTime"]>;
|
||||||
endTime: Dayjs;
|
endTime: Dayjs;
|
||||||
bypassBusyCalendarTimes: boolean;
|
bypassBusyCalendarTimes: boolean;
|
||||||
|
silentCalendarFailures: boolean;
|
||||||
shouldServeCache?: boolean;
|
shouldServeCache?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const usersWithCredentials = this.getUsersWithCredentials({
|
const usersWithCredentials = this.getUsersWithCredentials({
|
||||||
@@ -887,6 +890,7 @@ export class AvailableSlotsService {
|
|||||||
duration: input.duration || 0,
|
duration: input.duration || 0,
|
||||||
returnDateOverrides: false,
|
returnDateOverrides: false,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentlyHandleCalendarFailures: silentCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
},
|
},
|
||||||
initialData: {
|
initialData: {
|
||||||
@@ -958,6 +962,7 @@ export class AvailableSlotsService {
|
|||||||
const {
|
const {
|
||||||
_enableTroubleshooter: enableTroubleshooter = false,
|
_enableTroubleshooter: enableTroubleshooter = false,
|
||||||
_bypassCalendarBusyTimes: bypassBusyCalendarTimes = false,
|
_bypassCalendarBusyTimes: bypassBusyCalendarTimes = false,
|
||||||
|
_silentCalendarFailures: silentCalendarFailures = false,
|
||||||
_shouldServeCache,
|
_shouldServeCache,
|
||||||
routingFormResponseId,
|
routingFormResponseId,
|
||||||
queuedFormResponseId,
|
queuedFormResponseId,
|
||||||
@@ -1069,6 +1074,7 @@ export class AvailableSlotsService {
|
|||||||
? this.getStartTime(twoWeeksFromNow.format(), input.timeZone, eventType.minimumBookingNotice)
|
? this.getStartTime(twoWeeksFromNow.format(), input.timeZone, eventType.minimumBookingNotice)
|
||||||
: endTime,
|
: endTime,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1095,6 +1101,7 @@ export class AvailableSlotsService {
|
|||||||
startTime: dayjs(),
|
startTime: dayjs(),
|
||||||
endTime: twoWeeksFromNow,
|
endTime: twoWeeksFromNow,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
@@ -1129,6 +1136,7 @@ export class AvailableSlotsService {
|
|||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
bypassBusyCalendarTimes,
|
bypassBusyCalendarTimes,
|
||||||
|
silentCalendarFailures,
|
||||||
shouldServeCache,
|
shouldServeCache,
|
||||||
}));
|
}));
|
||||||
aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||||
@@ -1431,8 +1439,7 @@ export class AvailableSlotsService {
|
|||||||
// We only want to run this on single targeted events and not dynamic
|
// We only want to run this on single targeted events and not dynamic
|
||||||
if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) {
|
if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) {
|
||||||
try {
|
try {
|
||||||
// TODO: DI handleNotificationWhenNoSlots
|
await this.dependencies.noSlotsNotificationService.handleNotificationWhenNoSlots({
|
||||||
await handleNotificationWhenNoSlots({
|
|
||||||
eventDetails: {
|
eventDetails: {
|
||||||
username: input.usernameList?.[0],
|
username: input.usernameList?.[0],
|
||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
|
|||||||
@@ -2500,7 +2500,7 @@ __metadata:
|
|||||||
"@axiomhq/winston": ^1.2.0
|
"@axiomhq/winston": ^1.2.0
|
||||||
"@calcom/platform-constants": "*"
|
"@calcom/platform-constants": "*"
|
||||||
"@calcom/platform-enums": "*"
|
"@calcom/platform-enums": "*"
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306"
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312"
|
||||||
"@calcom/platform-types": "*"
|
"@calcom/platform-types": "*"
|
||||||
"@calcom/platform-utils": "*"
|
"@calcom/platform-utils": "*"
|
||||||
"@calcom/prisma": "*"
|
"@calcom/prisma": "*"
|
||||||
@@ -3559,13 +3559,13 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.306":
|
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.312":
|
||||||
version: 0.0.306
|
version: 0.0.312
|
||||||
resolution: "@calcom/platform-libraries@npm:0.0.306"
|
resolution: "@calcom/platform-libraries@npm:0.0.312"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@calcom/features": "*"
|
"@calcom/features": "*"
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "*"
|
||||||
checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9
|
checksum: 46520737404a61f3aa96c6f1adfa81e67f645fd984f495662ca7c63184b0310117a1dad57ddbe7ea487f2e5a0c9cbe62749a9a43b3197a021b19e3ccadc1f5b2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user