Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a | ||
|
|
0afe6cf5d1 | ||
|
|
75a161fe35 | ||
|
|
eb1ed107ba | ||
|
|
13e2e340ac | ||
|
|
0af62da16b | ||
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 |
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
|||||||
Under Vercel > Your API Project > Settings
|
Under Vercel > Your API Project > Settings
|
||||||
|
|
||||||
In General > Build & Development Settings
|
In General > Build & Development Settings
|
||||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||||
OUTPUT DIRECTORY: `apps/api/.next`
|
OUTPUT DIRECTORY: `apps/api/.next`
|
||||||
|
|
||||||
In Git > Ignored Build Step
|
In Git > Ignored Build Step
|
||||||
|
|||||||
@@ -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.314",
|
||||||
"@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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
|||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
// Get the membership first to get the userId
|
||||||
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({
|
||||||
|
teamIds: [organizationId],
|
||||||
|
userIds: [membership.userId],
|
||||||
|
isOrg: true,
|
||||||
|
});
|
||||||
|
|
||||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-4
@@ -20,7 +20,6 @@ import {
|
|||||||
OrgTeamMembershipOutputResponseDto,
|
OrgTeamMembershipOutputResponseDto,
|
||||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||||
private teamsEventTypesService: TeamsEventTypesService,
|
|
||||||
private readonly organizationsRepository: OrganizationsRepository
|
private readonly organizationsRepository: OrganizationsRepository
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
|||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
|||||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsTeamsMembershipsService {
|
export class OrganizationsTeamsMembershipsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
|
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
teamId,
|
teamId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
|||||||
EMAIL,
|
EMAIL,
|
||||||
WorkflowEmailAttendeeStepDto,
|
WorkflowEmailAttendeeStepDto,
|
||||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
import {
|
||||||
|
BEFORE_EVENT,
|
||||||
|
DAY,
|
||||||
|
OnAfterEventTriggerDto,
|
||||||
|
OnBeforeEventTriggerDto,
|
||||||
|
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||||
// Adjust path if needed
|
// Adjust path if needed
|
||||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
"+37255555555"
|
"+37255555555"
|
||||||
);
|
);
|
||||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||||
|
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
|
|
||||||
createdWorkflowId = responseBody.data.id;
|
createdWorkflowId = responseBody.data.id;
|
||||||
createdWorkflow = responseBody.data;
|
createdWorkflow = responseBody.data;
|
||||||
});
|
});
|
||||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
expect(step1).toBeDefined();
|
expect(step1).toBeDefined();
|
||||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||||
name: updatedName,
|
name: updatedName,
|
||||||
|
trigger: {
|
||||||
|
type: "afterEvent",
|
||||||
|
offset: {
|
||||||
|
unit: "minute",
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||||
};
|
};
|
||||||
expect(createdWorkflowId).toBeDefined();
|
expect(createdWorkflowId).toBeDefined();
|
||||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
|||||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||||
|
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||||
|
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||||
|
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||||
|
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
|||||||
|
|
||||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
|
||||||
try {
|
|
||||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
|
||||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error("Could not remove user from all team event-types.", {
|
|
||||||
error: err,
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
|||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
|
||||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
|||||||
export class TeamsMembershipsController {
|
export class TeamsMembershipsController {
|
||||||
private logger = new Logger("TeamsMembershipsController");
|
private logger = new Logger("TeamsMembershipsController");
|
||||||
|
|
||||||
constructor(
|
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||||
private teamsMembershipsService: TeamsMembershipsService,
|
|
||||||
private teamsEventTypesService: TeamsEventTypesService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
|||||||
membershipId,
|
membershipId,
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||||
try {
|
try {
|
||||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
|||||||
): Promise<DeleteTeamMembershipOutput> {
|
): Promise<DeleteTeamMembershipOutput> {
|
||||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||||
|
|
||||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
|||||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { TeamService } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamsMembershipsService {
|
export class TeamsMembershipsService {
|
||||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
// First get the membership to get the userId
|
||||||
teamId,
|
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||||
membershipId
|
|
||||||
);
|
if (!teamMembership) {
|
||||||
|
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||||
|
|
||||||
return teamMembership;
|
return teamMembership;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNumber, ValidateNested } from "class-validator";
|
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export const BEFORE_EVENT = "beforeEvent";
|
export const BEFORE_EVENT = "beforeEvent";
|
||||||
export const EVENT_CANCELLED = "eventCancelled";
|
export const EVENT_CANCELLED = "eventCancelled";
|
||||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
|||||||
value!: number;
|
value!: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||||
|
@IsString()
|
||||||
|
@IsIn(TIME_UNITS)
|
||||||
unit!: TimeUnitType;
|
unit!: TimeUnitType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||||
type!: WorkflowTriggerType;
|
type!: WorkflowTriggerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([NEW_EVENT])
|
||||||
type: typeof NEW_EVENT = NEW_EVENT;
|
type: typeof NEW_EVENT = NEW_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([RESCHEDULE_EVENT])
|
||||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||||
}
|
}
|
||||||
export class OnCancelTriggerDto {
|
export class OnCancelTriggerDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([EVENT_CANCELLED])
|
||||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: BEFORE_EVENT,
|
example: BEFORE_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([BEFORE_EVENT])
|
||||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_EVENT,
|
example: AFTER_EVENT,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_EVENT])
|
||||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
|||||||
description: "Trigger type for the workflow",
|
description: "Trigger type for the workflow",
|
||||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||||
})
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
|||||||
async delete(eventTypeId: EventType["id"]) {
|
async delete(eventTypeId: EventType["id"]) {
|
||||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(eventTypeId: EventType["id"]) {
|
||||||
|
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
|||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findById(membershipId: Membership["id"]) {
|
||||||
|
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,16 +35,23 @@ 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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||||
const installedCalendars = await appsCaller.integrations({
|
calendarsCaller.connectedCalendars(),
|
||||||
variant: "calendar",
|
appsCaller.integrations({
|
||||||
onlyInstalled: true,
|
variant: "calendar",
|
||||||
});
|
onlyInstalled: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstalledApps
|
<InstalledApps
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
+6
-12
@@ -1,21 +1,15 @@
|
|||||||
import type { GetServerSidePropsResult } from "next";
|
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 />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
|||||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
|
|||||||
const description = getDescription(t);
|
const description = getDescription(t);
|
||||||
const titleSuffix = `| ${APP_NAME}`;
|
const titleSuffix = `| ${APP_NAME}`;
|
||||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Script from "next/script";
|
|||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||||
import { IconSprites } from "@calcom/ui/components/icon";
|
import { IconSprites } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
|
|||||||
// Set canonical to https://cal.com or self-hosted URL
|
// Set canonical to https://cal.com or self-hosted URL
|
||||||
canonical={
|
canonical={
|
||||||
IS_CALCOM
|
IS_CALCOM
|
||||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
|
||||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||||
}
|
}
|
||||||
{...seoConfig.defaultNextSeo}
|
{...seoConfig.defaultNextSeo}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
error: null,
|
|
||||||
migrated: null,
|
|
||||||
userId: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const session = await getSession(ctx);
|
|
||||||
if (!session || !session.user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/login",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
|
||||||
if (!isAdmin) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
<div className="text-default mb-16 mt-4 block text-center text-sm">
|
||||||
{t("temporarily_out_of_office")}{" "}
|
{t("temporarily_out_of_office")}{" "}
|
||||||
<Link href="settings/my-account/out-of-office" className="underline">
|
<Link href="settings/my-account/out-of-office" className="underline">
|
||||||
{t("add_a_redirect")}
|
{t("add_a_redirect")}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.5.17",
|
"version": "5.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
|
|||||||
import {
|
import {
|
||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
confirmReschedule,
|
confirmReschedule,
|
||||||
|
confirmBooking,
|
||||||
doOnOrgDomain,
|
doOnOrgDomain,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
selectSecondAvailableTimeSlotNextMonth,
|
selectSecondAvailableTimeSlotNextMonth,
|
||||||
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
|
|||||||
activeState = await listItemLocator.getAttribute("data-active");
|
activeState = await listItemLocator.getAttribute("data-active");
|
||||||
expect(activeState).toEqual("false");
|
expect(activeState).toEqual("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await test.step("update event title to include duration placeholder", async () => {
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
|
await page.click(`text=Multiple duration`);
|
||||||
|
await page.waitForSelector('[data-testid="event-title"]');
|
||||||
|
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
||||||
|
"aria-current",
|
||||||
|
"page"
|
||||||
|
);
|
||||||
|
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||||
|
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||||
|
await page.locator('[data-testid="update-eventtype"]').click();
|
||||||
|
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/multiple-duration`);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
await test.step("verify default 30min duration is selected", async () => {
|
||||||
|
const duration30 = page.getByTestId("multiple-choice-30mins");
|
||||||
|
const activeState = await duration30.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("book with 90min duration and verify title", async () => {
|
||||||
|
await page.getByTestId("multiple-choice-90mins").click();
|
||||||
|
|
||||||
|
const duration90 = page.getByTestId("multiple-choice-90mins");
|
||||||
|
const activeState = await duration90.getAttribute("data-active");
|
||||||
|
expect(activeState).toEqual("true");
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.fill('[name="name"]', "Test User");
|
||||||
|
await page.fill('[name="email"]', "test@example.com");
|
||||||
|
await confirmBooking(page);
|
||||||
|
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
|
const bookingTitle = page.locator('[data-testid="booking-title"]');
|
||||||
|
await expect(bookingTitle).toBeVisible();
|
||||||
|
const titleText = await bookingTitle.textContent();
|
||||||
|
expect(titleText).toContain("90");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
test.skip("it contains the right event details", async ({ page }) => {
|
test.skip("it contains the right event details", async ({ page }) => {
|
||||||
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
|
|||||||
<Step title="Override the build command">
|
<Step title="Override the build command">
|
||||||
Override to:
|
Override to:
|
||||||
```
|
```
|
||||||
cd ../.. && yarn build --include-dependencies --no-deps
|
cd ../.. && yarn build --no-deps
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Hit Deploy">
|
<Step title="Hit Deploy">
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ RUN set -eux; \
|
|||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||||
yarn config set httpTimeout 1200000 && \
|
yarn config set httpTimeout 1200000 && \
|
||||||
npx turbo prune --scope=@calcom/web --docker && \
|
npx turbo prune --filter=@calcom/web --docker && \
|
||||||
npx turbo prune --scope=@calcom/api --docker && \
|
npx turbo prune --filter=@calcom/api --docker && \
|
||||||
yarn install && \
|
yarn install && \
|
||||||
yarn turbo run build --filter=@calcom/api
|
yarn turbo run build --filter=@calcom/api
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -28,23 +28,23 @@
|
|||||||
"edit-app-template": "yarn app-store edit-template",
|
"edit-app-template": "yarn app-store edit-template",
|
||||||
"delete-app-template": "yarn app-store delete-template",
|
"delete-app-template": "yarn app-store delete-template",
|
||||||
"build": "turbo run build --filter=@calcom/web...",
|
"build": "turbo run build --filter=@calcom/web...",
|
||||||
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
|
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
|
||||||
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
||||||
"db-deploy": "turbo run db-deploy",
|
"db-deploy": "turbo run db-deploy",
|
||||||
"db-seed": "turbo run db-seed",
|
"db-seed": "turbo run db-seed",
|
||||||
"db-studio": "yarn prisma studio",
|
"db-studio": "yarn prisma studio",
|
||||||
"deploy": "turbo run deploy",
|
"deploy": "turbo run deploy",
|
||||||
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
|
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
|
||||||
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
|
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
|
||||||
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
|
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
|
||||||
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
|
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
|
||||||
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
|
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
|
||||||
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
|
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
|
||||||
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
|
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
|
||||||
"dev": "turbo run dev --scope=\"@calcom/web\"",
|
"dev": "turbo run dev --filter=\"@calcom/web\"",
|
||||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
|
||||||
"dx": "turbo run dx",
|
"dx": "turbo run dx",
|
||||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
|
||||||
"i-dx": "infisical run -- turbo run dx",
|
"i-dx": "infisical run -- turbo run dx",
|
||||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"predev": "echo 'Checking env files'",
|
"predev": "echo 'Checking env files'",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
"start": "turbo run start --filter=\"@calcom/web\"",
|
||||||
"tdd": "vitest watch",
|
"tdd": "vitest watch",
|
||||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"turbo": "^1.10.1"
|
"turbo": "^2.5.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"types-ramda": "0.29.4",
|
"types-ramda": "0.29.4",
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const EventMeta = ({
|
|||||||
timeZones,
|
timeZones,
|
||||||
children,
|
children,
|
||||||
selectedTimeslot,
|
selectedTimeslot,
|
||||||
roundRobinHideOrgAndTeam
|
roundRobinHideOrgAndTeam,
|
||||||
}: {
|
}: {
|
||||||
event?: Pick<
|
event?: Pick<
|
||||||
BookerEvent,
|
BookerEvent,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
|
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
|
||||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
@@ -45,7 +45,7 @@ export const EventDuration = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
|
const [selectedDuration, setSelectedDuration, state] = useBookerStoreContext((state) => [
|
||||||
state.selectedDuration,
|
state.selectedDuration,
|
||||||
state.setSelectedDuration,
|
state.setSelectedDuration,
|
||||||
state.state,
|
state.state,
|
||||||
|
|||||||
@@ -550,6 +550,9 @@ async function handler(
|
|||||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||||
) {
|
) {
|
||||||
const bookingRepo = new BookingRepository(prisma);
|
const bookingRepo = new BookingRepository(prisma);
|
||||||
|
|
||||||
|
const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0;
|
||||||
|
|
||||||
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
bookerEmail,
|
bookerEmail,
|
||||||
@@ -559,13 +562,20 @@ async function handler(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingBooking) {
|
if (existingBooking) {
|
||||||
|
const hasPayments = existingBooking.payment.length > 0;
|
||||||
|
const isPaidBooking = existingBooking.paid || !hasPayments;
|
||||||
|
|
||||||
|
const shouldShowPaymentForm = requiresPayment && !isPaidBooking;
|
||||||
|
|
||||||
|
const firstPayment = shouldShowPaymentForm ? existingBooking.payment[0] : undefined;
|
||||||
|
|
||||||
const bookingResponse = {
|
const bookingResponse = {
|
||||||
...existingBooking,
|
...existingBooking,
|
||||||
user: {
|
user: {
|
||||||
...existingBooking.user,
|
...existingBooking.user,
|
||||||
email: null,
|
email: null,
|
||||||
},
|
},
|
||||||
paymentRequired: false,
|
paymentRequired: shouldShowPaymentForm,
|
||||||
seatReferenceUid: "",
|
seatReferenceUid: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -574,8 +584,8 @@ async function handler(
|
|||||||
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
|
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
|
||||||
isDryRun,
|
isDryRun,
|
||||||
...(isDryRun ? { troubleshooterData } : {}),
|
...(isDryRun ? { troubleshooterData } : {}),
|
||||||
paymentUid: undefined,
|
paymentUid: firstPayment?.uid,
|
||||||
paymentId: undefined,
|
paymentId: firstPayment?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,6 +1440,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 +1714,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.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
*
|
*
|
||||||
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
|
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
|
||||||
*/
|
*/
|
||||||
|
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createBookingScenario,
|
createBookingScenario,
|
||||||
getDate,
|
getDate,
|
||||||
@@ -3277,6 +3279,122 @@ describe("handleNewBooking", () => {
|
|||||||
},
|
},
|
||||||
timeout
|
timeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`,
|
||||||
|
async ({ emails }) => {
|
||||||
|
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||||
|
const booker = getBooker({
|
||||||
|
email: "booker@example.com",
|
||||||
|
name: "Booker",
|
||||||
|
});
|
||||||
|
|
||||||
|
const organizer = getOrganizer({
|
||||||
|
name: "Organizer",
|
||||||
|
email: "organizer@example.com",
|
||||||
|
id: 101,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential(), getStripeAppCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenarioData = getScenarioData({
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Paid Event",
|
||||||
|
description: "It's a test Paid Event",
|
||||||
|
slotInterval: 30,
|
||||||
|
requiresConfirmation: false,
|
||||||
|
metadata: {
|
||||||
|
apps: {
|
||||||
|
stripe: {
|
||||||
|
price: 100,
|
||||||
|
enabled: true,
|
||||||
|
currency: "usd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
length: 30,
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizer,
|
||||||
|
apps: [TestData.apps["stripe-payment"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createBookingScenario(scenarioData);
|
||||||
|
mockSuccessfulVideoMeetingCreation({
|
||||||
|
metadataLookupKey: "dailyvideo",
|
||||||
|
});
|
||||||
|
const { paymentUid } = mockPaymentApp({
|
||||||
|
metadataLookupKey: "stripe",
|
||||||
|
appStoreLookupKey: "stripepayment",
|
||||||
|
});
|
||||||
|
mockCalendarToHaveNoBusySlots("googlecalendar");
|
||||||
|
|
||||||
|
const mockBookingData = getMockRequestDataForBooking({
|
||||||
|
data: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
responses: {
|
||||||
|
email: booker.email,
|
||||||
|
name: booker.name,
|
||||||
|
location: { optionValue: "", value: "New York" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstBooking = await handleNewBooking({
|
||||||
|
bookingData: mockBookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBooking).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentUid: expect.any(String),
|
||||||
|
paymentRequired: true,
|
||||||
|
uid: expect.any(String),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBookingInDb = await prismaMock.booking.findUnique({
|
||||||
|
where: { uid: firstBooking.uid },
|
||||||
|
include: { payment: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(firstBookingInDb).toBeTruthy();
|
||||||
|
expect(firstBookingInDb?.payment).toHaveLength(1);
|
||||||
|
expect(firstBookingInDb?.paid).toBe(false);
|
||||||
|
|
||||||
|
const secondBooking = await handleNewBooking({
|
||||||
|
bookingData: mockBookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(secondBooking.uid).toBe(firstBooking.uid);
|
||||||
|
expect(secondBooking.paymentUid).toBe(firstBooking.paymentUid);
|
||||||
|
expect(secondBooking.paymentRequired).toBe(true);
|
||||||
|
|
||||||
|
const bookingsInDb = await prismaMock.booking.findMany({
|
||||||
|
where: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: booker.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { payment: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(bookingsInDb).toHaveLength(1);
|
||||||
|
expect(bookingsInDb[0].payment).toHaveLength(1);
|
||||||
|
expect(bookingsInDb[0].uid).toBe(firstBooking.uid);
|
||||||
|
},
|
||||||
|
timeout
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
import { TeamService } from "@calcom/lib/server/service/teamService";
|
||||||
|
|
||||||
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
||||||
return removeMember({
|
return TeamService.removeMembers({ teamIds: [orgId], userIds: [userId], isOrg: true });
|
||||||
memberId: userId,
|
|
||||||
teamId: orgId,
|
|
||||||
isOrg: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default removeUserFromOrg;
|
export default removeUserFromOrg;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
|
|
||||||
import { deleteWorkfowRemindersOfRemovedMember } from "./deleteWorkflowRemindersOfRemovedMember";
|
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["removeMember"] });
|
|
||||||
|
|
||||||
const removeMember = async ({
|
|
||||||
memberId,
|
|
||||||
teamId,
|
|
||||||
isOrg,
|
|
||||||
}: {
|
|
||||||
memberId: number;
|
|
||||||
teamId: number;
|
|
||||||
isOrg: boolean;
|
|
||||||
}) => {
|
|
||||||
const [membership] = await prisma.$transaction([
|
|
||||||
prisma.membership.delete({
|
|
||||||
where: {
|
|
||||||
userId_teamId: { userId: memberId, teamId: teamId },
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
team: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// remove user as host from team events associated with this membership
|
|
||||||
prisma.host.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: memberId,
|
|
||||||
eventType: {
|
|
||||||
teamId: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: { id: teamId },
|
|
||||||
select: {
|
|
||||||
isOrganization: true,
|
|
||||||
organizationSettings: true,
|
|
||||||
id: true,
|
|
||||||
metadata: true,
|
|
||||||
activeOrgWorkflows: true,
|
|
||||||
parentId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const foundUser = await prisma.user.findUnique({
|
|
||||||
where: { id: memberId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
movedToProfileId: true,
|
|
||||||
email: true,
|
|
||||||
username: true,
|
|
||||||
completedOnboarding: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
parentId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!team || !foundUser) throw new TRPCError({ code: "NOT_FOUND" });
|
|
||||||
|
|
||||||
if (isOrg) {
|
|
||||||
log.debug("Removing a member from the organization");
|
|
||||||
// Deleting membership from all child teams
|
|
||||||
// Delete all sub-team memberships where this team is the organization
|
|
||||||
await prisma.membership.deleteMany({
|
|
||||||
where: {
|
|
||||||
team: {
|
|
||||||
parentId: teamId,
|
|
||||||
},
|
|
||||||
userId: membership.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const userToDeleteMembershipOf = foundUser;
|
|
||||||
|
|
||||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
|
||||||
userId: userToDeleteMembershipOf.id,
|
|
||||||
organizationId: team.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
userToDeleteMembershipOf.username &&
|
|
||||||
userToDeleteMembershipOf.movedToProfileId === profileToDelete?.id
|
|
||||||
) {
|
|
||||||
log.debug("Cleaning up tempOrgRedirect for user", userToDeleteMembershipOf.username);
|
|
||||||
|
|
||||||
await prisma.tempOrgRedirect.deleteMany({
|
|
||||||
where: {
|
|
||||||
from: userToDeleteMembershipOf.username,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.user.update({
|
|
||||||
where: { id: membership.userId },
|
|
||||||
data: { organizationId: null },
|
|
||||||
}),
|
|
||||||
ProfileRepository.delete({
|
|
||||||
userId: membership.userId,
|
|
||||||
organizationId: team.id,
|
|
||||||
}),
|
|
||||||
prisma.host.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: memberId,
|
|
||||||
eventType: {
|
|
||||||
team: {
|
|
||||||
parentId: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleted managed event types from this team from this member
|
|
||||||
await prisma.eventType.deleteMany({
|
|
||||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteWorkfowRemindersOfRemovedMember(team, memberId, isOrg);
|
|
||||||
|
|
||||||
return { membership };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default removeMember;
|
|
||||||
@@ -110,13 +110,15 @@ describe("RoleManagementFactory", () => {
|
|||||||
it("should allow role change when user has permission", async () => {
|
it("should allow role change when user has permission", async () => {
|
||||||
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
await expect(
|
||||||
|
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||||
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
||||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||||
new RoleManagementError(
|
new RoleManagementError(
|
||||||
"You do not have permission to change roles",
|
"You do not have permission to change roles",
|
||||||
RoleManagementErrorCode.UNAUTHORIZED
|
RoleManagementErrorCode.UNAUTHORIZED
|
||||||
@@ -193,13 +195,15 @@ describe("RoleManagementFactory", () => {
|
|||||||
customRoleId: null,
|
customRoleId: null,
|
||||||
});
|
});
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
await expect(
|
||||||
|
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||||
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
||||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
||||||
const manager = await factory.createRoleManager(organizationId);
|
const manager = await factory.createRoleManager(organizationId);
|
||||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||||
new RoleManagementError(
|
new RoleManagementError(
|
||||||
"Only owners or admin can update roles",
|
"Only owners or admin can update roles",
|
||||||
RoleManagementErrorCode.UNAUTHORIZED
|
RoleManagementErrorCode.UNAUTHORIZED
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||||
|
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import { RoleService } from "./role.service";
|
|||||||
|
|
||||||
interface IRoleManager {
|
interface IRoleManager {
|
||||||
isPBACEnabled: boolean;
|
isPBACEnabled: boolean;
|
||||||
checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void>;
|
checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void>;
|
||||||
assignRole(
|
assignRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
organizationId: number,
|
organizationId: number,
|
||||||
@@ -29,11 +30,11 @@ class PBACRoleManager implements IRoleManager {
|
|||||||
private readonly permissionCheckService: PermissionCheckService
|
private readonly permissionCheckService: PermissionCheckService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||||
const hasPermission = await this.permissionCheckService.checkPermission({
|
const hasPermission = await this.permissionCheckService.checkPermission({
|
||||||
userId,
|
userId,
|
||||||
teamId: organizationId,
|
teamId: targetId,
|
||||||
permission: "organization.changeMemberRole",
|
permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
|
||||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,9 +96,11 @@ class PBACRoleManager implements IRoleManager {
|
|||||||
|
|
||||||
class LegacyRoleManager implements IRoleManager {
|
class LegacyRoleManager implements IRoleManager {
|
||||||
public isPBACEnabled = false;
|
public isPBACEnabled = false;
|
||||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||||
const membership = await isOrganisationAdmin(userId, organizationId);
|
const membership =
|
||||||
|
scope === "team"
|
||||||
|
? !!(await isTeamAdmin(userId, targetId))
|
||||||
|
: !!(await isOrganisationAdmin(userId, targetId));
|
||||||
// Only OWNER/ADMIN can update role
|
// Only OWNER/ADMIN can update role
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new RoleManagementError(
|
throw new RoleManagementError(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export const IS_CALCOM =
|
|||||||
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
|
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
|
||||||
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com"));
|
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com") ||
|
||||||
|
new URL(WEBAPP_URL).hostname.endsWith("cal.eu"));
|
||||||
|
|
||||||
export const CONSOLE_URL =
|
export const CONSOLE_URL =
|
||||||
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
|
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type {
|
|||||||
InsightsRoutingServicePublicOptions,
|
InsightsRoutingServicePublicOptions,
|
||||||
InsightsRoutingServiceFilterOptions,
|
InsightsRoutingServiceFilterOptions,
|
||||||
InsightsRoutingBaseService,
|
InsightsRoutingBaseService,
|
||||||
} from "@calcom/lib/server/service/insightsRoutingBase";
|
} from "@calcom/lib/server/service/InsightsRoutingBaseService";
|
||||||
import type { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import type { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||||
|
|
||||||
import { insightsRoutingModule } from "../modules/InsightsRouting";
|
import { insightsRoutingModule } from "../modules/InsightsRouting";
|
||||||
|
|||||||
@@ -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>);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createModule } from "@evyweb/ioctopus";
|
import { createModule } from "@evyweb/ioctopus";
|
||||||
|
|
||||||
import type { IInsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import type { IInsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
import { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
import { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||||
|
|
||||||
import { DI_TOKENS } from "../tokens";
|
import { DI_TOKENS } from "../tokens";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { WEBAPP_URL, IS_CALCOM } from "./constants";
|
||||||
|
|
||||||
|
export const getCalcomUrl = () => {
|
||||||
|
if (IS_CALCOM) {
|
||||||
|
return new URL(WEBAPP_URL).hostname.endsWith("cal.eu") ? "https://cal.eu" : "https://cal.com";
|
||||||
|
}
|
||||||
|
return WEBAPP_URL;
|
||||||
|
};
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ export class BookingRepository {
|
|||||||
attendees: true,
|
attendees: true,
|
||||||
references: true,
|
references: true,
|
||||||
user: true,
|
user: true,
|
||||||
|
payment: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
-1
@@ -4,7 +4,7 @@ import {
|
|||||||
InsightsRoutingBaseService,
|
InsightsRoutingBaseService,
|
||||||
type InsightsRoutingServicePublicOptions,
|
type InsightsRoutingServicePublicOptions,
|
||||||
type InsightsRoutingServiceFilterOptions,
|
type InsightsRoutingServiceFilterOptions,
|
||||||
} from "./insightsRoutingBase";
|
} from "./InsightsRoutingBaseService";
|
||||||
|
|
||||||
export interface IInsightsRoutingService {
|
export interface IInsightsRoutingService {
|
||||||
prisma: typeof readonlyPrisma;
|
prisma: typeof readonlyPrisma;
|
||||||
@@ -7,7 +7,7 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/insightsRoutingBase";
|
import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/InsightsRoutingBaseService";
|
||||||
|
|
||||||
// SQL condition constants for testing
|
// SQL condition constants for testing
|
||||||
const NOTHING_CONDITION = Prisma.sql`1=0`;
|
const NOTHING_CONDITION = Prisma.sql`1=0`;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,52 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { TeamBilling } from "@calcom/features/ee/billing/teams";
|
import { TeamBilling } from "@calcom/features/ee/billing/teams";
|
||||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
import { deleteWorkfowRemindersOfRemovedMember } from "@calcom/features/ee/teams/lib/deleteWorkflowRemindersOfRemovedMember";
|
||||||
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Membership } from "@calcom/prisma/client";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
const log = logger.getSubLogger({ prefix: ["TeamService"] });
|
||||||
|
|
||||||
|
type MembershipWithRelations = Pick<
|
||||||
|
Membership,
|
||||||
|
"id" | "userId" | "teamId" | "role" | "accepted" | "disableImpersonation"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type TeamWithSettings = {
|
||||||
|
id: number;
|
||||||
|
isOrganization: boolean | null;
|
||||||
|
organizationSettings: unknown;
|
||||||
|
metadata: unknown;
|
||||||
|
activeOrgWorkflows: unknown;
|
||||||
|
parentId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserWithTeams = {
|
||||||
|
id: number;
|
||||||
|
movedToProfileId: number | null;
|
||||||
|
email: string;
|
||||||
|
username: string | null;
|
||||||
|
completedOnboarding: boolean;
|
||||||
|
teams: {
|
||||||
|
team: {
|
||||||
|
id: number;
|
||||||
|
parentId: number | null;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveMemberResult = {
|
||||||
|
membership: MembershipWithRelations;
|
||||||
|
};
|
||||||
|
|
||||||
export class TeamService {
|
export class TeamService {
|
||||||
/**
|
/**
|
||||||
* Deletes a team and all its associated data in a safe, transactional order.
|
* Deletes a team and all its associated data in a safe, transactional order.
|
||||||
@@ -45,15 +81,23 @@ export class TeamService {
|
|||||||
return deletedTeam;
|
return deletedTeam;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
|
static async removeMembers({
|
||||||
const deleteMembershipPromises = [];
|
teamIds,
|
||||||
|
userIds,
|
||||||
|
isOrg = false,
|
||||||
|
}: {
|
||||||
|
teamIds: number[];
|
||||||
|
userIds: number[];
|
||||||
|
isOrg?: boolean;
|
||||||
|
}) {
|
||||||
|
const deleteMembershipPromises: Promise<RemoveMemberResult>[] = [];
|
||||||
|
|
||||||
for (const memberId of memberIds) {
|
for (const userId of userIds) {
|
||||||
for (const teamId of teamIds) {
|
for (const teamId of teamIds) {
|
||||||
deleteMembershipPromises.push(
|
deleteMembershipPromises.push(
|
||||||
removeMember({
|
TeamService.removeMember({
|
||||||
teamId,
|
teamId,
|
||||||
memberId,
|
userId,
|
||||||
isOrg,
|
isOrg,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -121,4 +165,217 @@ export class TeamService {
|
|||||||
const teamBilling = await TeamBilling.findAndInit(teamId);
|
const teamBilling = await TeamBilling.findAndInit(teamId);
|
||||||
return teamBilling.publish();
|
return teamBilling.publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async removeMember({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
isOrg,
|
||||||
|
}: {
|
||||||
|
userId: number;
|
||||||
|
teamId: number;
|
||||||
|
isOrg: boolean;
|
||||||
|
}) {
|
||||||
|
const membership = await TeamService.fetchMembershipOrThrow(userId, teamId);
|
||||||
|
const team = await TeamService.fetchTeamOrThrow(teamId);
|
||||||
|
const user = await TeamService.fetchUserOrThrow(userId);
|
||||||
|
|
||||||
|
if (isOrg) {
|
||||||
|
log.debug("Removing a member from the organization");
|
||||||
|
await TeamService.removeFromOrganization(membership, team, user);
|
||||||
|
} else {
|
||||||
|
log.debug("Removing a member from a team");
|
||||||
|
await TeamService.removeFromTeam(membership, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteWorkfowRemindersOfRemovedMember(team, userId, isOrg);
|
||||||
|
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Needs to be moved to repository
|
||||||
|
private static async fetchMembershipOrThrow(
|
||||||
|
userId: number,
|
||||||
|
teamId: number
|
||||||
|
): Promise<MembershipWithRelations> {
|
||||||
|
const membership = await prisma.membership.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_teamId: { userId: userId, teamId: teamId },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
teamId: true,
|
||||||
|
role: true,
|
||||||
|
accepted: true,
|
||||||
|
disableImpersonation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Membership not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Needs to be moved to repository
|
||||||
|
private static async fetchTeamOrThrow(teamId: number): Promise<TeamWithSettings> {
|
||||||
|
const team = await prisma.team.findUnique({
|
||||||
|
where: { id: teamId },
|
||||||
|
select: {
|
||||||
|
isOrganization: true,
|
||||||
|
organizationSettings: true,
|
||||||
|
id: true,
|
||||||
|
metadata: true,
|
||||||
|
activeOrgWorkflows: true,
|
||||||
|
parentId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Needs to be moved to repository
|
||||||
|
private static async fetchUserOrThrow(userId: number): Promise<UserWithTeams> {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
movedToProfileId: true,
|
||||||
|
email: true,
|
||||||
|
username: true,
|
||||||
|
completedOnboarding: true,
|
||||||
|
teams: {
|
||||||
|
select: {
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
parentId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Needs to be moved to repository
|
||||||
|
private static async cleanupTempOrgRedirect(user: UserWithTeams, team: TeamWithSettings) {
|
||||||
|
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
||||||
|
userId: user.id,
|
||||||
|
organizationId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user.username && user.movedToProfileId === profileToDelete?.id) {
|
||||||
|
log.debug("Cleaning up tempOrgRedirect for user", user.username);
|
||||||
|
await prisma.tempOrgRedirect.deleteMany({
|
||||||
|
where: {
|
||||||
|
from: user.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async removeFromOrganization(
|
||||||
|
membership: MembershipWithRelations,
|
||||||
|
team: TeamWithSettings,
|
||||||
|
user: UserWithTeams
|
||||||
|
) {
|
||||||
|
await TeamService.cleanupTempOrgRedirect(user, team);
|
||||||
|
const newUsername = generateNewUsername(user);
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
// Remove user from all sub-teams event type hosts
|
||||||
|
prisma.host.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: membership.userId,
|
||||||
|
eventType: {
|
||||||
|
team: {
|
||||||
|
parentId: team.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Delete managed child events in sub-teams
|
||||||
|
prisma.eventType.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: membership.userId,
|
||||||
|
parent: {
|
||||||
|
team: {
|
||||||
|
parentId: team.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Remove organizationId from the user
|
||||||
|
prisma.user.update({
|
||||||
|
where: { id: membership.userId },
|
||||||
|
data: {
|
||||||
|
organizationId: null,
|
||||||
|
username: newUsername,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Delete the profile of the user from the organization
|
||||||
|
ProfileRepository.delete({
|
||||||
|
userId: membership.userId,
|
||||||
|
organizationId: team.id,
|
||||||
|
}),
|
||||||
|
// Delete all sub-team memberships where this team is the organization
|
||||||
|
prisma.membership.deleteMany({
|
||||||
|
where: {
|
||||||
|
team: {
|
||||||
|
parentId: team.id,
|
||||||
|
},
|
||||||
|
userId: membership.userId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Delete the membership of the user from the organization
|
||||||
|
prisma.membership.delete({
|
||||||
|
where: {
|
||||||
|
userId_teamId: { userId: membership.userId, teamId: team.id },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Generate new username for user leaving organization
|
||||||
|
function generateNewUsername(user: UserWithTeams): string | null {
|
||||||
|
// We ensure that new username would be unique across all users in the global namespace outside any organization
|
||||||
|
return user.username != null ? `${user.username}-${user.id}` : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove member from regular team
|
||||||
|
private static async removeFromTeam(membership: MembershipWithRelations, teamId: number) {
|
||||||
|
await prisma.$transaction([
|
||||||
|
// Remove user from all team event types' hosts
|
||||||
|
prisma.host.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: membership.userId,
|
||||||
|
eventType: {
|
||||||
|
teamId: teamId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Deleted managed event types from this team for this member
|
||||||
|
prisma.eventType.deleteMany({
|
||||||
|
where: { parent: { teamId: teamId }, userId: membership.userId },
|
||||||
|
}),
|
||||||
|
// Delete the membership of the user from the team
|
||||||
|
prisma.membership.delete({
|
||||||
|
where: {
|
||||||
|
userId_teamId: { userId: membership.userId, teamId: teamId },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -120,4 +120,5 @@ export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
|||||||
|
|
||||||
export { verifyPhoneNumber, sendVerificationCode };
|
export { verifyPhoneNumber, sendVerificationCode };
|
||||||
|
|
||||||
|
export { TeamService } from "@calcom/lib/server/service/teamService";
|
||||||
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||||
|
|||||||
@@ -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}` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
|||||||
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
|
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await roleManager.checkPermissionToChangeRole(userId, organizationId);
|
await roleManager.checkPermissionToChangeRole(userId, organizationId, "org");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RoleManagementError) {
|
if (error instanceof RoleManagementError) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||||
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
|
import { isTeamOwner } from "@calcom/lib/server/queries/teams";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
@@ -32,7 +32,7 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
|
|||||||
|
|
||||||
// Check permission to change roles
|
// Check permission to change roles
|
||||||
try {
|
try {
|
||||||
await roleManager.checkPermissionToChangeRole(ctx.user.id, organizationId);
|
await roleManager.checkPermissionToChangeRole(ctx.user.id, input.teamId, "team");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
@@ -45,8 +45,6 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
|
|||||||
typeof input.role === "string" &&
|
typeof input.role === "string" &&
|
||||||
Object.values(MembershipRole).includes(input.role as MembershipRole)
|
Object.values(MembershipRole).includes(input.role as MembershipRole)
|
||||||
) {
|
) {
|
||||||
// Traditional role assignment logic
|
|
||||||
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
// Only owners can award owner role.
|
// Only owners can award owner role.
|
||||||
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
|
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
|
|||||||
message: "You can not remove yourself from a team you own.",
|
message: "You can not remove yourself from a team you own.",
|
||||||
});
|
});
|
||||||
|
|
||||||
await TeamService.removeMembers(teamIds, memberIds, isOrg);
|
await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg });
|
||||||
};
|
};
|
||||||
|
|
||||||
export default removeMemberHandler;
|
export default removeMemberHandler;
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
||||||
// @ts-nocheck
|
|
||||||
// TODO: Bring this test back with the correct setup (no illegal imports)
|
|
||||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
|
||||||
|
|
||||||
import { describe, test, expect } from "vitest";
|
|
||||||
|
|
||||||
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
import type { TrpcSessionUser } from "../../../types";
|
|
||||||
import removeMember from "./removeMember.handler";
|
|
||||||
|
|
||||||
describe.skip("removeMember", () => {
|
|
||||||
describe("should remove a member from a team", () => {
|
|
||||||
test(`1) Should remove a member from a team
|
|
||||||
2) Should remove the member from hosts
|
|
||||||
`, async () => {
|
|
||||||
const org = await createOrganization({
|
|
||||||
name: "Test Org",
|
|
||||||
slug: "testorg",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the child team
|
|
||||||
const childTeam = {
|
|
||||||
id: 202,
|
|
||||||
name: "Team 1",
|
|
||||||
slug: "team-1",
|
|
||||||
parentId: org.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const organizer = getOrganizer({
|
|
||||||
name: "Organizer",
|
|
||||||
email: "organizer@example.com",
|
|
||||||
id: 101,
|
|
||||||
organizationId: org.id,
|
|
||||||
schedules: [TestData.schedules.IstWorkHours],
|
|
||||||
teams: [
|
|
||||||
{
|
|
||||||
membership: {
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.ADMIN,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
id: org.id,
|
|
||||||
name: "Test Org",
|
|
||||||
slug: "testorg",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
membership: {
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.ADMIN,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
id: childTeam.id,
|
|
||||||
name: "Team 1",
|
|
||||||
slug: "team-1",
|
|
||||||
parentId: org.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const otherTeamMembers = [
|
|
||||||
{
|
|
||||||
name: "Other Team Member 1",
|
|
||||||
username: "other-team-member-1",
|
|
||||||
timeZone: Timezones["+5:30"],
|
|
||||||
defaultScheduleId: null,
|
|
||||||
email: "other-team-member-1@example.com",
|
|
||||||
id: 102,
|
|
||||||
organizationId: org.id,
|
|
||||||
schedules: [TestData.schedules.IstEveningShift],
|
|
||||||
teams: [
|
|
||||||
{
|
|
||||||
membership: {
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.MEMBER,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
id: org.id,
|
|
||||||
name: "Test Org",
|
|
||||||
slug: "testorg",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
membership: {
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.MEMBER,
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
id: childTeam.id,
|
|
||||||
name: "Team 1",
|
|
||||||
slug: "team-1",
|
|
||||||
parentId: org.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await createBookingScenario(
|
|
||||||
getScenarioData(
|
|
||||||
{
|
|
||||||
eventTypes: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
teamId: childTeam.id,
|
|
||||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
||||||
length: 30,
|
|
||||||
hosts: [
|
|
||||||
{
|
|
||||||
userId: 101,
|
|
||||||
isFixed: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: 102,
|
|
||||||
isFixed: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
organizer,
|
|
||||||
usersApartFromOrganizer: otherTeamMembers,
|
|
||||||
apps: [TestData.apps["daily-video"]],
|
|
||||||
},
|
|
||||||
org
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
//Logged in user is admin of the org
|
|
||||||
const ctx = {
|
|
||||||
user: {
|
|
||||||
id: organizer.id,
|
|
||||||
name: organizer.name,
|
|
||||||
} as NonNullable<TrpcSessionUser>,
|
|
||||||
};
|
|
||||||
|
|
||||||
await removeMember({
|
|
||||||
ctx,
|
|
||||||
input: {
|
|
||||||
teamIds: [org.id],
|
|
||||||
memberIds: [102],
|
|
||||||
isOrg: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//Check if the remaining memberships are correct
|
|
||||||
const remainingMemberships = await prismaMock.membership.count({
|
|
||||||
where: {
|
|
||||||
teamId: childTeam.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(remainingMemberships).toBe(1);
|
|
||||||
|
|
||||||
//Check if the event type has the correct hosts
|
|
||||||
const remainingHostsCount = await prismaMock.host.count({
|
|
||||||
where: {
|
|
||||||
eventTypeId: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(remainingHostsCount).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/react-library.json",
|
"extends": "@calcom/tsconfig/react-library.json",
|
||||||
"paths": {
|
|
||||||
"~/*": ["/*"],
|
|
||||||
"@calcom/app-store": ["../app-store"],
|
|
||||||
"@calcom/app-store/*": ["../app-store/*"],
|
|
||||||
"@calcom/dayjs": ["../dayjs"],
|
|
||||||
"@calcom/dayjs/*": ["../dayjs/*"],
|
|
||||||
"@calcom/features": ["../features"],
|
|
||||||
"@calcom/features/*": ["../features/*"],
|
|
||||||
"@calcom/lib": ["../lib"],
|
|
||||||
"@calcom/lib/*": ["../lib/*"],
|
|
||||||
"@calcom/prisma": ["../prisma"],
|
|
||||||
"@calcom/prisma/*": ["../prisma/*"],
|
|
||||||
"@calcom/repository/*": ["../lib/server/repository/*"]
|
|
||||||
},
|
|
||||||
"include": [
|
"include": [
|
||||||
"../types/@wojtekmaj__react-daterange-picker.d.ts",
|
"../types/@wojtekmaj__react-daterange-picker.d.ts",
|
||||||
"../types/business-days-plugin.d.ts",
|
"../types/business-days-plugin.d.ts",
|
||||||
@@ -33,6 +19,9 @@
|
|||||||
"declarationMap": false,
|
"declarationMap": false,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"declarationDir": "types/server",
|
"declarationDir": "types/server",
|
||||||
"moduleResolution": "node"
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+254
-253
@@ -1,6 +1,257 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turborepo.org/schema.json",
|
"$schema": "https://turborepo.org/schema.json",
|
||||||
"pipeline": {
|
"globalDependencies": ["yarn.lock"],
|
||||||
|
"globalEnv": [
|
||||||
|
"ALLOWED_HOSTNAMES",
|
||||||
|
"ANALYZE",
|
||||||
|
"API_KEY_PREFIX",
|
||||||
|
"APP_USER_NAME",
|
||||||
|
"ATOMS_E2E_API_URL",
|
||||||
|
"ATOMS_E2E_OAUTH_CLIENT_ID",
|
||||||
|
"ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED",
|
||||||
|
"ATOMS_E2E_OAUTH_CLIENT_SECRET",
|
||||||
|
"ATOMS_E2E_ORG_ID",
|
||||||
|
"BASECAMP3_CLIENT_ID",
|
||||||
|
"BASECAMP3_CLIENT_SECRET",
|
||||||
|
"BASECAMP3_USER_AGENT",
|
||||||
|
"BLACKLISTED_GUEST_EMAILS",
|
||||||
|
"AUTH_BEARER_TOKEN_VERCEL",
|
||||||
|
"BUILD_ID",
|
||||||
|
"CAL_SIGNATURE_TOKEN",
|
||||||
|
"CALCOM_PRIVATE_API_ROUTE",
|
||||||
|
"CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",
|
||||||
|
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
|
||||||
|
"CALCOM_CREDENTIAL_SYNC_SECRET",
|
||||||
|
"CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME",
|
||||||
|
"CALCOM_ENV",
|
||||||
|
"CALCOM_LICENSE_KEY",
|
||||||
|
"CALCOM_QA_EMAIL",
|
||||||
|
"CALCOM_QA_PASSWORD",
|
||||||
|
"CALCOM_TELEMETRY_DISABLED",
|
||||||
|
"CALCOM_CREDENTIAL_SYNC_HEADER_NAME",
|
||||||
|
"CALENDSO_ENCRYPTION_KEY",
|
||||||
|
"CALCOM_CREDENTIAL_SYNC_SECRET",
|
||||||
|
"CI",
|
||||||
|
"CLOSECOM_CLIENT_ID",
|
||||||
|
"CLOSECOM_CLIENT_SECRET",
|
||||||
|
"CRON_API_KEY",
|
||||||
|
"CRON_SECRET",
|
||||||
|
"CRON_ENABLE_APP_SYNC",
|
||||||
|
"CLOUDFLARE_TURNSTILE_SECRET",
|
||||||
|
"DAILY_API_KEY",
|
||||||
|
"DAILY_SCALE_PLAN",
|
||||||
|
"DAILY_WEBHOOK_SECRET",
|
||||||
|
"DAILY_MEETING_ENDED_WEBHOOK_SECRET",
|
||||||
|
"DAILY_VIDEO_REGION",
|
||||||
|
"DATABASE_CHUNK_SIZE",
|
||||||
|
"DATABASE_DIRECT_URL",
|
||||||
|
"DATABASE_URL",
|
||||||
|
"DEBUG",
|
||||||
|
"DUB_API_KEY",
|
||||||
|
"NEXT_PUBLIC_DUB_PROGRAM_ID",
|
||||||
|
"E2E_TEST_APPLE_CALENDAR_EMAIL",
|
||||||
|
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
|
||||||
|
"E2E_TEST_CALCOM_QA_EMAIL",
|
||||||
|
"E2E_TEST_CALCOM_QA_PASSWORD",
|
||||||
|
"E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS",
|
||||||
|
"E2E_TEST_CALCOM_GCAL_KEYS",
|
||||||
|
"E2E_TEST_MAILHOG_ENABLED",
|
||||||
|
"E2E_TEST_OIDC_CLIENT_ID",
|
||||||
|
"E2E_TEST_OIDC_CLIENT_SECRET",
|
||||||
|
"E2E_TEST_OIDC_PROVIDER_DOMAIN",
|
||||||
|
"E2E_TEST_OIDC_USER_EMAIL",
|
||||||
|
"E2E_TEST_OIDC_USER_PASSWORD",
|
||||||
|
"E2E_TEST_SAML_ADMIN_EMAIL",
|
||||||
|
"E2E_TEST_SAML_ADMIN_PASSWORD",
|
||||||
|
"EMAIL_FROM",
|
||||||
|
"EMAIL_FROM_NAME",
|
||||||
|
"EMAIL_SERVER_HOST",
|
||||||
|
"EMAIL_SERVER_PASSWORD",
|
||||||
|
"EMAIL_SERVER_PORT",
|
||||||
|
"EMAIL_SERVER_USER",
|
||||||
|
"EMAIL_SERVER",
|
||||||
|
"ENABLE_EXPANDED_CACHE",
|
||||||
|
"EXCHANGE_DEFAULT_EWS_URL",
|
||||||
|
"FORMBRICKS_FEEDBACK_SURVEY_ID",
|
||||||
|
"AVATARAPI_USERNAME",
|
||||||
|
"AVATARAPI_PASSWORD",
|
||||||
|
"GIPHY_API_KEY",
|
||||||
|
"GITHUB_API_REPO_TOKEN",
|
||||||
|
"GOOGLE_API_CREDENTIALS",
|
||||||
|
"GOOGLE_LOGIN_ENABLED",
|
||||||
|
"GOOGLE_WEBHOOK_TOKEN",
|
||||||
|
"HEROKU_APP_NAME",
|
||||||
|
"HUBSPOT_CLIENT_ID",
|
||||||
|
"HUBSPOT_CLIENT_SECRET",
|
||||||
|
"IFFY_API_KEY",
|
||||||
|
"INTEGRATION_TEST_MODE",
|
||||||
|
"IS_E2E",
|
||||||
|
"INTERCOM_SECRET",
|
||||||
|
"INTERCOM_SECRET",
|
||||||
|
"INSIGHTS_DATABASE_URL",
|
||||||
|
"IP_BANLIST",
|
||||||
|
"LARK_OPEN_APP_ID",
|
||||||
|
"LARK_OPEN_APP_SECRET",
|
||||||
|
"LARK_OPEN_VERIFICATION_TOKEN",
|
||||||
|
"MOCK_PAYMENT_APP_ENABLED",
|
||||||
|
"MS_GRAPH_CLIENT_ID",
|
||||||
|
"MS_GRAPH_CLIENT_SECRET",
|
||||||
|
"NEXT_PUBLIC_API_URL",
|
||||||
|
"NEXT_PUBLIC_APP_NAME",
|
||||||
|
"NEXT_PUBLIC_AUTH_URL",
|
||||||
|
"NEXT_PUBLIC_CALCOM_VERSION",
|
||||||
|
"NEXT_PUBLIC_COMPANY_NAME",
|
||||||
|
"NEXT_PUBLIC_CONSOLE_URL",
|
||||||
|
"NEXT_PUBLIC_LOGGER_LEVEL",
|
||||||
|
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
||||||
|
"NEXT_PUBLIC_EMBED_LIB_URL",
|
||||||
|
"NEXT_PUBLIC_FORMBRICKS_HOST_URL",
|
||||||
|
"NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID",
|
||||||
|
"NEXT_PUBLIC_HOSTED_CAL_FEATURES",
|
||||||
|
"NEXT_PUBLIC_MINUTES_TO_BOOK",
|
||||||
|
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
||||||
|
"NEXT_PUBLIC_SENDER_ID",
|
||||||
|
"NEXT_PUBLIC_SENDGRID_SENDER_NAME",
|
||||||
|
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID",
|
||||||
|
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_PUBLISHABLE_KEY",
|
||||||
|
"NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS",
|
||||||
|
"NEXT_PUBLIC_SWAGGER_DOCS_URL",
|
||||||
|
"NEXT_PUBLIC_TEAM_IMPERSONATION",
|
||||||
|
"NEXT_PUBLIC_VERCEL_URL",
|
||||||
|
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
|
||||||
|
"NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER",
|
||||||
|
"NEXT_PUBLIC_VERCEL_ENV",
|
||||||
|
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
|
||||||
|
"NEXT_PUBLIC_GTM_ID",
|
||||||
|
"NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS",
|
||||||
|
"NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW",
|
||||||
|
"NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL",
|
||||||
|
"NEXT_PUBLIC_WEBSITE_TERMS_URL",
|
||||||
|
"NEXT_RUNTIME",
|
||||||
|
"NEXTAUTH_COOKIE_DOMAIN",
|
||||||
|
"NEXTAUTH_SECRET",
|
||||||
|
"NEXTAUTH_URL",
|
||||||
|
"NODE_ENV",
|
||||||
|
"ORGANIZATIONS_ENABLED",
|
||||||
|
"ORGANIZATIONS_AUTOLINK",
|
||||||
|
"PAYMENT_FEE_FIXED",
|
||||||
|
"PAYMENT_FEE_PERCENTAGE",
|
||||||
|
"PLAYWRIGHT_HEADLESS",
|
||||||
|
"PLAYWRIGHT_TEST_BASE_URL",
|
||||||
|
"PRISMA_FIELD_ENCRYPTION_KEY",
|
||||||
|
"PRISMA_GENERATE_DATAPROXY",
|
||||||
|
"PROJECT_ID_VERCEL",
|
||||||
|
"QUICK",
|
||||||
|
"RAILWAY_STATIC_URL",
|
||||||
|
"RENDER_EXTERNAL_URL",
|
||||||
|
"RESERVED_SUBDOMAINS",
|
||||||
|
"RETELL_AI_KEY",
|
||||||
|
"SALESFORCE_CONSUMER_KEY",
|
||||||
|
"SALESFORCE_CONSUMER_SECRET",
|
||||||
|
"SAML_ADMINS",
|
||||||
|
"SAML_CLIENT_SECRET_VERIFIER",
|
||||||
|
"SAML_DATABASE_URL",
|
||||||
|
"SEND_FEEDBACK_EMAIL",
|
||||||
|
"SENDGRID_API_KEY",
|
||||||
|
"SENDGRID_EMAIL",
|
||||||
|
"SENDGRID_SYNC_API_KEY",
|
||||||
|
"SKIP_DB_MIGRATIONS",
|
||||||
|
"SLACK_CLIENT_ID",
|
||||||
|
"SLACK_CLIENT_SECRET",
|
||||||
|
"SLACK_SIGNING_SECRET",
|
||||||
|
"STRIPE_CLIENT_ID",
|
||||||
|
"STRIPE_PRIVATE_KEY",
|
||||||
|
"STRIPE_PRODUCT_ID_ESSENTIALS",
|
||||||
|
"STRIPE_PRODUCT_ID_SCALE",
|
||||||
|
"STRIPE_PRODUCT_ID_STARTER",
|
||||||
|
"STRIPE_WEBHOOK_SECRET",
|
||||||
|
"STRIPE_WEBHOOK_SECRET_APPS",
|
||||||
|
"STRIPE_WEBHOOK_SECRET_BILLING",
|
||||||
|
"STRIPE_TEAM_MONTHLY_PRICE_ID",
|
||||||
|
"TANDEM_BASE_URL",
|
||||||
|
"TANDEM_CLIENT_ID",
|
||||||
|
"TANDEM_CLIENT_SECRET",
|
||||||
|
"TASKER_ENABLE_WEBHOOKS",
|
||||||
|
"TEAM_ID_VERCEL",
|
||||||
|
"TELEMETRY_DEBUG",
|
||||||
|
"TWILIO_MESSAGING_SID",
|
||||||
|
"TWILIO_OPT_OUT_ENABLED",
|
||||||
|
"TWILIO_PHONE_NUMBER",
|
||||||
|
"TWILIO_WHATSAPP_PHONE_NUMBER",
|
||||||
|
"TWILIO_SID",
|
||||||
|
"TWILIO_TOKEN",
|
||||||
|
"TWILIO_VERIFY_SID",
|
||||||
|
"TWILIO_WHATSAPP_REMINDER_CONTENT_SID",
|
||||||
|
"TWILIO_WHATSAPP_CANCELLED_CONTENT_SID",
|
||||||
|
"TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID",
|
||||||
|
"TWILIO_WHATSAPP_COMPLETED_CONTENT_SID",
|
||||||
|
"UPSTASH_REDIS_REST_TOKEN",
|
||||||
|
"UPSTASH_REDIS_REST_URL",
|
||||||
|
"UNKEY_ROOT_KEY",
|
||||||
|
"USERNAME_BLACKLIST_URL",
|
||||||
|
"VERCEL_ENV",
|
||||||
|
"VERCEL_URL",
|
||||||
|
"VITAL_API_KEY",
|
||||||
|
"VITAL_DEVELOPMENT_MODE",
|
||||||
|
"VITAL_REGION",
|
||||||
|
"VITAL_WEBHOOK_SECRET",
|
||||||
|
"ZAPIER_INVITE_LINK",
|
||||||
|
"ZOHOCRM_CLIENT_ID",
|
||||||
|
"ZOHOCRM_CLIENT_SECRET",
|
||||||
|
"ZOOM_CLIENT_ID",
|
||||||
|
"ZOOM_CLIENT_SECRET",
|
||||||
|
"REVERT_API_KEY",
|
||||||
|
"REVERT_API_URL",
|
||||||
|
"REVERT_PUBLIC_TOKEN",
|
||||||
|
"RESEND_API_KEY",
|
||||||
|
"LOCAL_TESTING_DOMAIN_VERCEL",
|
||||||
|
"AUTH_BEARER_TOKEN_CLOUDFLARE",
|
||||||
|
"CLOUDFLARE_ZONE_ID",
|
||||||
|
"NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY",
|
||||||
|
"NEXT_PUBLIC_CHAT_API_URL",
|
||||||
|
"NEXT_PUBLIC_DOCS_URL",
|
||||||
|
"CLOUDFLARE_VERCEL_CNAME",
|
||||||
|
"CLOUDFLARE_DNS",
|
||||||
|
"EMBED_PUBLIC_EMBED_FINGER_PRINT",
|
||||||
|
"EMBED_PUBLIC_EMBED_VERSION",
|
||||||
|
"EMBED_PUBLIC_WEBAPP_URL",
|
||||||
|
"EMBED_PUBLIC_VERCEL_URL",
|
||||||
|
"EMBED_PUBLIC_EMBED_LIB_URL",
|
||||||
|
"NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER",
|
||||||
|
"NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS",
|
||||||
|
"NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS",
|
||||||
|
"NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS",
|
||||||
|
"NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM",
|
||||||
|
"NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT",
|
||||||
|
"NEXT_PUBLIC_HEAD_SCRIPTS",
|
||||||
|
"NEXT_PUBLIC_BODY_SCRIPTS",
|
||||||
|
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
||||||
|
"NEXT_PUBLIC_API_V2_ROOT_URL",
|
||||||
|
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
|
||||||
|
"VAPID_PRIVATE_KEY",
|
||||||
|
"CAL_VIDEO_BUCKET_NAME",
|
||||||
|
"CAL_VIDEO_BUCKET_REGION",
|
||||||
|
"CAL_VIDEO_ASSUME_ROLE_ARN",
|
||||||
|
"CAL_VIDEO_MEETING_LINK_FOR_TESTING",
|
||||||
|
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||||
|
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||||
|
"VAPID_PRIVATE_KEY",
|
||||||
|
"HUDDLE01_API_TOKEN",
|
||||||
|
"LINGO_DOT_DEV_API_KEY",
|
||||||
|
"DIRECTORY_IDS_TO_LOG",
|
||||||
|
"NEXT_PUBLIC_SINGLE_ORG_SLUG",
|
||||||
|
"GOOGLE_REFRESH_TOKEN",
|
||||||
|
"GOOGLE_CLIENT_ID",
|
||||||
|
"GOOGLE_CLIENT_SECRET",
|
||||||
|
"ZOOM_REFRESH_TOKEN",
|
||||||
|
"CALCOM_ADMIN_API_KEY",
|
||||||
|
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
|
||||||
|
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
|
||||||
|
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
|
||||||
|
"ORGANIZER_EMAIL_EXEMPT_DOMAINS",
|
||||||
|
"SLOTS_CACHE_TTL"
|
||||||
|
],
|
||||||
|
"tasks": {
|
||||||
"@calcom/prisma#build": {
|
"@calcom/prisma#build": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"dependsOn": ["post-install"]
|
"dependsOn": ["post-install"]
|
||||||
@@ -207,7 +458,7 @@
|
|||||||
},
|
},
|
||||||
"@calcom/app-store-cli#build": {
|
"@calcom/app-store-cli#build": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"inputs": ["../../app-store/**/**"],
|
"inputs": ["../../app-store/**"],
|
||||||
"outputs": ["../../app-store/apps.server.generated.ts", "../../app-store/apps.browser.generated.tsx"]
|
"outputs": ["../../app-store/apps.server.generated.ts", "../../app-store/apps.browser.generated.tsx"]
|
||||||
},
|
},
|
||||||
"@calcom/embed-react#type-check": {
|
"@calcom/embed-react#type-check": {
|
||||||
@@ -247,255 +498,5 @@
|
|||||||
"//#test": {
|
"//#test": {
|
||||||
"cache": false
|
"cache": false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"globalDependencies": ["yarn.lock"],
|
|
||||||
"globalEnv": [
|
|
||||||
"ALLOWED_HOSTNAMES",
|
|
||||||
"ANALYZE",
|
|
||||||
"API_KEY_PREFIX",
|
|
||||||
"APP_USER_NAME",
|
|
||||||
"ATOMS_E2E_API_URL",
|
|
||||||
"ATOMS_E2E_OAUTH_CLIENT_ID",
|
|
||||||
"ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED",
|
|
||||||
"ATOMS_E2E_OAUTH_CLIENT_SECRET",
|
|
||||||
"ATOMS_E2E_ORG_ID",
|
|
||||||
"BASECAMP3_CLIENT_ID",
|
|
||||||
"BASECAMP3_CLIENT_SECRET",
|
|
||||||
"BASECAMP3_USER_AGENT",
|
|
||||||
"BLACKLISTED_GUEST_EMAILS",
|
|
||||||
"AUTH_BEARER_TOKEN_VERCEL",
|
|
||||||
"BUILD_ID",
|
|
||||||
"CAL_SIGNATURE_TOKEN",
|
|
||||||
"CALCOM_PRIVATE_API_ROUTE",
|
|
||||||
"CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",
|
|
||||||
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
|
|
||||||
"CALCOM_CREDENTIAL_SYNC_SECRET",
|
|
||||||
"CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME",
|
|
||||||
"CALCOM_ENV",
|
|
||||||
"CALCOM_LICENSE_KEY",
|
|
||||||
"CALCOM_QA_EMAIL",
|
|
||||||
"CALCOM_QA_PASSWORD",
|
|
||||||
"CALCOM_TELEMETRY_DISABLED",
|
|
||||||
"CALCOM_CREDENTIAL_SYNC_HEADER_NAME",
|
|
||||||
"CALENDSO_ENCRYPTION_KEY",
|
|
||||||
"CALCOM_CREDENTIAL_SYNC_SECRET",
|
|
||||||
"CI",
|
|
||||||
"CLOSECOM_CLIENT_ID",
|
|
||||||
"CLOSECOM_CLIENT_SECRET",
|
|
||||||
"CRON_API_KEY",
|
|
||||||
"CRON_SECRET",
|
|
||||||
"CRON_ENABLE_APP_SYNC",
|
|
||||||
"CLOUDFLARE_TURNSTILE_SECRET",
|
|
||||||
"DAILY_API_KEY",
|
|
||||||
"DAILY_SCALE_PLAN",
|
|
||||||
"DAILY_WEBHOOK_SECRET",
|
|
||||||
"DAILY_MEETING_ENDED_WEBHOOK_SECRET",
|
|
||||||
"DAILY_VIDEO_REGION",
|
|
||||||
"DATABASE_CHUNK_SIZE",
|
|
||||||
"DATABASE_DIRECT_URL",
|
|
||||||
"DATABASE_URL",
|
|
||||||
"DEBUG",
|
|
||||||
"DUB_API_KEY",
|
|
||||||
"NEXT_PUBLIC_DUB_PROGRAM_ID",
|
|
||||||
"E2E_TEST_APPLE_CALENDAR_EMAIL",
|
|
||||||
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
|
|
||||||
"E2E_TEST_CALCOM_QA_EMAIL",
|
|
||||||
"E2E_TEST_CALCOM_QA_PASSWORD",
|
|
||||||
"E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS",
|
|
||||||
"E2E_TEST_CALCOM_GCAL_KEYS",
|
|
||||||
"E2E_TEST_MAILHOG_ENABLED",
|
|
||||||
"E2E_TEST_OIDC_CLIENT_ID",
|
|
||||||
"E2E_TEST_OIDC_CLIENT_SECRET",
|
|
||||||
"E2E_TEST_OIDC_PROVIDER_DOMAIN",
|
|
||||||
"E2E_TEST_OIDC_USER_EMAIL",
|
|
||||||
"E2E_TEST_OIDC_USER_PASSWORD",
|
|
||||||
"E2E_TEST_SAML_ADMIN_EMAIL",
|
|
||||||
"E2E_TEST_SAML_ADMIN_PASSWORD",
|
|
||||||
"EMAIL_FROM",
|
|
||||||
"EMAIL_FROM_NAME",
|
|
||||||
"EMAIL_SERVER_HOST",
|
|
||||||
"EMAIL_SERVER_PASSWORD",
|
|
||||||
"EMAIL_SERVER_PORT",
|
|
||||||
"EMAIL_SERVER_USER",
|
|
||||||
"EMAIL_SERVER",
|
|
||||||
"ENABLE_EXPANDED_CACHE",
|
|
||||||
"EXCHANGE_DEFAULT_EWS_URL",
|
|
||||||
"FORMBRICKS_FEEDBACK_SURVEY_ID",
|
|
||||||
"AVATARAPI_USERNAME",
|
|
||||||
"AVATARAPI_PASSWORD",
|
|
||||||
"GIPHY_API_KEY",
|
|
||||||
"GITHUB_API_REPO_TOKEN",
|
|
||||||
"GOOGLE_API_CREDENTIALS",
|
|
||||||
"GOOGLE_LOGIN_ENABLED",
|
|
||||||
"GOOGLE_WEBHOOK_TOKEN",
|
|
||||||
"HEROKU_APP_NAME",
|
|
||||||
"HUBSPOT_CLIENT_ID",
|
|
||||||
"HUBSPOT_CLIENT_SECRET",
|
|
||||||
"IFFY_API_KEY",
|
|
||||||
"INTEGRATION_TEST_MODE",
|
|
||||||
"IS_E2E",
|
|
||||||
"INTERCOM_SECRET",
|
|
||||||
"INTERCOM_SECRET",
|
|
||||||
"INSIGHTS_DATABASE_URL",
|
|
||||||
"IP_BANLIST",
|
|
||||||
"LARK_OPEN_APP_ID",
|
|
||||||
"LARK_OPEN_APP_SECRET",
|
|
||||||
"LARK_OPEN_VERIFICATION_TOKEN",
|
|
||||||
"MOCK_PAYMENT_APP_ENABLED",
|
|
||||||
"MS_GRAPH_CLIENT_ID",
|
|
||||||
"MS_GRAPH_CLIENT_SECRET",
|
|
||||||
"NEXT_PUBLIC_API_URL",
|
|
||||||
"NEXT_PUBLIC_APP_NAME",
|
|
||||||
"NEXT_PUBLIC_AUTH_URL",
|
|
||||||
"NEXT_PUBLIC_CALCOM_VERSION",
|
|
||||||
"NEXT_PUBLIC_COMPANY_NAME",
|
|
||||||
"NEXT_PUBLIC_CONSOLE_URL",
|
|
||||||
"NEXT_PUBLIC_LOGGER_LEVEL",
|
|
||||||
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
|
||||||
"NEXT_PUBLIC_EMBED_LIB_URL",
|
|
||||||
"NEXT_PUBLIC_FORMBRICKS_HOST_URL",
|
|
||||||
"NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID",
|
|
||||||
"NEXT_PUBLIC_HOSTED_CAL_FEATURES",
|
|
||||||
"NEXT_PUBLIC_MINUTES_TO_BOOK",
|
|
||||||
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
|
||||||
"NEXT_PUBLIC_SENDER_ID",
|
|
||||||
"NEXT_PUBLIC_SENDGRID_SENDER_NAME",
|
|
||||||
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID",
|
|
||||||
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_PUBLISHABLE_KEY",
|
|
||||||
"NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS",
|
|
||||||
"NEXT_PUBLIC_SWAGGER_DOCS_URL",
|
|
||||||
"NEXT_PUBLIC_TEAM_IMPERSONATION",
|
|
||||||
"NEXT_PUBLIC_VERCEL_URL",
|
|
||||||
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
|
|
||||||
"NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER",
|
|
||||||
"NEXT_PUBLIC_VERCEL_ENV",
|
|
||||||
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
|
|
||||||
"NEXT_PUBLIC_GTM_ID",
|
|
||||||
"NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS",
|
|
||||||
"NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW",
|
|
||||||
"NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL",
|
|
||||||
"NEXT_PUBLIC_WEBSITE_TERMS_URL",
|
|
||||||
"NEXT_RUNTIME",
|
|
||||||
"NEXTAUTH_COOKIE_DOMAIN",
|
|
||||||
"NEXTAUTH_SECRET",
|
|
||||||
"NEXTAUTH_URL",
|
|
||||||
"NODE_ENV",
|
|
||||||
"ORGANIZATIONS_ENABLED",
|
|
||||||
"ORGANIZATIONS_AUTOLINK",
|
|
||||||
"PAYMENT_FEE_FIXED",
|
|
||||||
"PAYMENT_FEE_PERCENTAGE",
|
|
||||||
"PLAYWRIGHT_HEADLESS",
|
|
||||||
"PLAYWRIGHT_TEST_BASE_URL",
|
|
||||||
"PRISMA_FIELD_ENCRYPTION_KEY",
|
|
||||||
"PRISMA_GENERATE_DATAPROXY",
|
|
||||||
"PROJECT_ID_VERCEL",
|
|
||||||
"QUICK",
|
|
||||||
"RAILWAY_STATIC_URL",
|
|
||||||
"RENDER_EXTERNAL_URL",
|
|
||||||
"RESERVED_SUBDOMAINS",
|
|
||||||
"RETELL_AI_KEY",
|
|
||||||
"SALESFORCE_CONSUMER_KEY",
|
|
||||||
"SALESFORCE_CONSUMER_SECRET",
|
|
||||||
"SAML_ADMINS",
|
|
||||||
"SAML_CLIENT_SECRET_VERIFIER",
|
|
||||||
"SAML_DATABASE_URL",
|
|
||||||
"SEND_FEEDBACK_EMAIL",
|
|
||||||
"SENDGRID_API_KEY",
|
|
||||||
"SENDGRID_EMAIL",
|
|
||||||
"SENDGRID_SYNC_API_KEY",
|
|
||||||
"SKIP_DB_MIGRATIONS",
|
|
||||||
"SLACK_CLIENT_ID",
|
|
||||||
"SLACK_CLIENT_SECRET",
|
|
||||||
"SLACK_SIGNING_SECRET",
|
|
||||||
"STRIPE_CLIENT_ID",
|
|
||||||
"STRIPE_PRIVATE_KEY",
|
|
||||||
"STRIPE_PRODUCT_ID_ESSENTIALS",
|
|
||||||
"STRIPE_PRODUCT_ID_SCALE",
|
|
||||||
"STRIPE_PRODUCT_ID_STARTER",
|
|
||||||
"STRIPE_WEBHOOK_SECRET",
|
|
||||||
"STRIPE_WEBHOOK_SECRET_APPS",
|
|
||||||
"STRIPE_WEBHOOK_SECRET_BILLING",
|
|
||||||
"TANDEM_BASE_URL",
|
|
||||||
"TANDEM_CLIENT_ID",
|
|
||||||
"TANDEM_CLIENT_SECRET",
|
|
||||||
"TASKER_ENABLE_WEBHOOKS",
|
|
||||||
"TEAM_ID_VERCEL",
|
|
||||||
"TELEMETRY_DEBUG",
|
|
||||||
"TWILIO_MESSAGING_SID",
|
|
||||||
"TWILIO_OPT_OUT_ENABLED",
|
|
||||||
"TWILIO_PHONE_NUMBER",
|
|
||||||
"TWILIO_WHATSAPP_PHONE_NUMBER",
|
|
||||||
"TWILIO_SID",
|
|
||||||
"TWILIO_TOKEN",
|
|
||||||
"TWILIO_VERIFY_SID",
|
|
||||||
"TWILIO_WHATSAPP_REMINDER_CONTENT_SID",
|
|
||||||
"TWILIO_WHATSAPP_CANCELLED_CONTENT_SID",
|
|
||||||
"TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID",
|
|
||||||
"TWILIO_WHATSAPP_COMPLETED_CONTENT_SID",
|
|
||||||
"UPSTASH_REDIS_REST_TOKEN",
|
|
||||||
"UPSTASH_REDIS_REST_URL",
|
|
||||||
"UNKEY_ROOT_KEY",
|
|
||||||
"USERNAME_BLACKLIST_URL",
|
|
||||||
"VERCEL_ENV",
|
|
||||||
"VERCEL_URL",
|
|
||||||
"VITAL_API_KEY",
|
|
||||||
"VITAL_DEVELOPMENT_MODE",
|
|
||||||
"VITAL_REGION",
|
|
||||||
"VITAL_WEBHOOK_SECRET",
|
|
||||||
"ZAPIER_INVITE_LINK",
|
|
||||||
"ZOHOCRM_CLIENT_ID",
|
|
||||||
"ZOHOCRM_CLIENT_SECRET",
|
|
||||||
"ZOOM_CLIENT_ID",
|
|
||||||
"ZOOM_CLIENT_SECRET",
|
|
||||||
"REVERT_API_KEY",
|
|
||||||
"REVERT_API_URL",
|
|
||||||
"REVERT_PUBLIC_TOKEN",
|
|
||||||
"RESEND_API_KEY",
|
|
||||||
"LOCAL_TESTING_DOMAIN_VERCEL",
|
|
||||||
"AUTH_BEARER_TOKEN_CLOUDFLARE",
|
|
||||||
"CLOUDFLARE_ZONE_ID",
|
|
||||||
"NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY",
|
|
||||||
"NEXT_PUBLIC_CHAT_API_URL",
|
|
||||||
"NEXT_PUBLIC_DOCS_URL",
|
|
||||||
"CLOUDFLARE_VERCEL_CNAME",
|
|
||||||
"CLOUDFLARE_DNS",
|
|
||||||
"EMBED_PUBLIC_EMBED_FINGER_PRINT",
|
|
||||||
"EMBED_PUBLIC_EMBED_VERSION",
|
|
||||||
"EMBED_PUBLIC_WEBAPP_URL",
|
|
||||||
"EMBED_PUBLIC_VERCEL_URL",
|
|
||||||
"EMBED_PUBLIC_EMBED_LIB_URL",
|
|
||||||
"NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER",
|
|
||||||
"NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS",
|
|
||||||
"NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS",
|
|
||||||
"NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS",
|
|
||||||
"NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM",
|
|
||||||
"NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT",
|
|
||||||
"NEXT_PUBLIC_HEAD_SCRIPTS",
|
|
||||||
"NEXT_PUBLIC_BODY_SCRIPTS",
|
|
||||||
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
|
||||||
"NEXT_PUBLIC_API_V2_ROOT_URL",
|
|
||||||
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
|
|
||||||
"VAPID_PRIVATE_KEY",
|
|
||||||
"CAL_VIDEO_BUCKET_NAME",
|
|
||||||
"CAL_VIDEO_BUCKET_REGION",
|
|
||||||
"CAL_VIDEO_ASSUME_ROLE_ARN",
|
|
||||||
"CAL_VIDEO_MEETING_LINK_FOR_TESTING",
|
|
||||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
|
||||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
|
||||||
"VAPID_PRIVATE_KEY",
|
|
||||||
"HUDDLE01_API_TOKEN",
|
|
||||||
"LINGO_DOT_DEV_API_KEY",
|
|
||||||
"DIRECTORY_IDS_TO_LOG",
|
|
||||||
"NEXT_PUBLIC_SINGLE_ORG_SLUG",
|
|
||||||
"GOOGLE_REFRESH_TOKEN",
|
|
||||||
"GOOGLE_CLIENT_ID",
|
|
||||||
"GOOGLE_CLIENT_SECRET",
|
|
||||||
"ZOOM_REFRESH_TOKEN",
|
|
||||||
"CALCOM_ADMIN_API_KEY",
|
|
||||||
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
|
|
||||||
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
|
|
||||||
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
|
|
||||||
"ORGANIZER_EMAIL_EXEMPT_DOMAINS",
|
|
||||||
"SLOTS_CACHE_TTL"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.314"
|
||||||
"@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.314":
|
||||||
version: 0.0.306
|
version: 0.0.314
|
||||||
resolution: "@calcom/platform-libraries@npm:0.0.306"
|
resolution: "@calcom/platform-libraries@npm:0.0.314"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@calcom/features": "*"
|
"@calcom/features": "*"
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "*"
|
||||||
checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9
|
checksum: b59eb591273334ed7d7d07a8f0518eb8cb40255883dff49626f85d3bd804db4da357bf77cd4f7b14ae0743d0a699647232b39956af9123bdc5e7b2458cab30eb
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -21694,7 +21694,7 @@ __metadata:
|
|||||||
prismock: ^1.33.4
|
prismock: ^1.33.4
|
||||||
resize-observer-polyfill: ^1.5.1
|
resize-observer-polyfill: ^1.5.1
|
||||||
tsc-absolute: ^1.0.0
|
tsc-absolute: ^1.0.0
|
||||||
turbo: ^1.10.1
|
turbo: ^2.5.5
|
||||||
typescript: 5.9.0-beta
|
typescript: 5.9.0-beta
|
||||||
vitest: ^2.1.1
|
vitest: ^2.1.1
|
||||||
vitest-fetch-mock: ^0.3.0
|
vitest-fetch-mock: ^0.3.0
|
||||||
@@ -44926,58 +44926,58 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-darwin-64@npm:1.10.1":
|
"turbo-darwin-64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-darwin-64@npm:1.10.1"
|
resolution: "turbo-darwin-64@npm:2.5.5"
|
||||||
conditions: os=darwin & cpu=x64
|
conditions: os=darwin & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-darwin-arm64@npm:1.10.1":
|
"turbo-darwin-arm64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-darwin-arm64@npm:1.10.1"
|
resolution: "turbo-darwin-arm64@npm:2.5.5"
|
||||||
conditions: os=darwin & cpu=arm64
|
conditions: os=darwin & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-linux-64@npm:1.10.1":
|
"turbo-linux-64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-linux-64@npm:1.10.1"
|
resolution: "turbo-linux-64@npm:2.5.5"
|
||||||
conditions: os=linux & cpu=x64
|
conditions: os=linux & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-linux-arm64@npm:1.10.1":
|
"turbo-linux-arm64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-linux-arm64@npm:1.10.1"
|
resolution: "turbo-linux-arm64@npm:2.5.5"
|
||||||
conditions: os=linux & cpu=arm64
|
conditions: os=linux & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-windows-64@npm:1.10.1":
|
"turbo-windows-64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-windows-64@npm:1.10.1"
|
resolution: "turbo-windows-64@npm:2.5.5"
|
||||||
conditions: os=win32 & cpu=x64
|
conditions: os=win32 & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo-windows-arm64@npm:1.10.1":
|
"turbo-windows-arm64@npm:2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo-windows-arm64@npm:1.10.1"
|
resolution: "turbo-windows-arm64@npm:2.5.5"
|
||||||
conditions: os=win32 & cpu=arm64
|
conditions: os=win32 & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"turbo@npm:^1.10.1":
|
"turbo@npm:^2.5.5":
|
||||||
version: 1.10.1
|
version: 2.5.5
|
||||||
resolution: "turbo@npm:1.10.1"
|
resolution: "turbo@npm:2.5.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
turbo-darwin-64: 1.10.1
|
turbo-darwin-64: 2.5.5
|
||||||
turbo-darwin-arm64: 1.10.1
|
turbo-darwin-arm64: 2.5.5
|
||||||
turbo-linux-64: 1.10.1
|
turbo-linux-64: 2.5.5
|
||||||
turbo-linux-arm64: 1.10.1
|
turbo-linux-arm64: 2.5.5
|
||||||
turbo-windows-64: 1.10.1
|
turbo-windows-64: 2.5.5
|
||||||
turbo-windows-arm64: 1.10.1
|
turbo-windows-arm64: 2.5.5
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
turbo-darwin-64:
|
turbo-darwin-64:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -44993,7 +44993,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
turbo: bin/turbo
|
turbo: bin/turbo
|
||||||
checksum: 3551c57be67e833583de549d74d5ec991e0f1e38047e25600b8acc085a1a8a4007992259a015eb9f2c0abd68156545800bd34e7b5ad337d1998b32461952d3fb
|
checksum: 8da6525097ba17bb59571eb512f216362b046bbf88765ddb05fb9937a44e04eeba3bb883c55fca53a7e8a104e02366b4f6f99ef90d12078d0dc95d4659004af6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user