fix: eventType atom team availability tab (#17158)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
1 parent
59aff41ca3
commit
995bf09dde
17 files changed
+553
-17
No files matched your search
@@ -0,0 +1,42 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserInOrgTeam implements CanActivate {
|
||||
constructor(private organizationsRepository: OrganizationsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
|
||||
const teamId: string = request.params.teamId;
|
||||
const orgId: string = request.params.orgId;
|
||||
const userId: string = request.params.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ForbiddenException("No user id found in request params.");
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
throw new ForbiddenException("No org id found in request params.");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new ForbiddenException("No team id found in request params.");
|
||||
}
|
||||
|
||||
const user = await this.organizationsRepository.findOrgTeamUser(
|
||||
Number(orgId),
|
||||
Number(teamId),
|
||||
Number(userId)
|
||||
);
|
||||
|
||||
if (user) {
|
||||
request.user = user;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -2,6 +2,7 @@ import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrgTeamMembershipDto } from "@/modules/organizations/inputs/create-organization-team-membership.input";
|
||||
import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/inputs/update-organization-team-membership.input";
|
||||
import { OrgTeamMembershipOutputDto } from "@/modules/organizations/outputs/organization-teams-memberships.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -37,7 +38,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let nonOrgTeam: Team;
|
||||
let membership: Membership;
|
||||
let membership2: Membership;
|
||||
let membershipCreatedViaApi: Membership;
|
||||
let membershipCreatedViaApi: OrgTeamMembershipOutputDto;
|
||||
|
||||
const userEmail = "org-admin-membership-teams-controller-e2e@api.com";
|
||||
const userEmail2 = "org-member-membership-teams-controller-e2e@api.com";
|
||||
@@ -200,7 +201,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership.id);
|
||||
expect(responseBody.data[1].id).toEqual(membership2.id);
|
||||
@@ -219,7 +220,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships?skip=1&take=1`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership[]> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data[0].id).toEqual(membership2.id);
|
||||
expect(responseBody.data[0].userId).toEqual(user2.id);
|
||||
@@ -238,10 +239,11 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membership.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membership.id);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
expect(responseBody.data.user.email).toEqual(user.email);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -272,12 +274,13 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
} satisfies CreateOrgTeamMembershipDto)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.teamId).toEqual(orgTeam.id);
|
||||
expect(membershipCreatedViaApi.role).toEqual("MEMBER");
|
||||
expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id);
|
||||
expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,7 +303,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
} satisfies UpdateOrgTeamMembershipDto)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
membershipCreatedViaApi = responseBody.data;
|
||||
expect(membershipCreatedViaApi.role).toEqual("OWNER");
|
||||
@@ -312,7 +315,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.delete(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships/${membershipCreatedViaApi.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Membership> = response.body;
|
||||
const responseBody: ApiSuccessResponse<OrgTeamMembershipOutputDto> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.id).toEqual(membershipCreatedViaApi.id);
|
||||
});
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, IsTeamInOrg, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Orgs / Teams / Schedules")
|
||||
export class OrganizationsTeamsSchedulesController {
|
||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrgTeam)
|
||||
@Get("/users/:userId/schedules")
|
||||
@DocsTags("Orgs / Teams / Users / Schedules")
|
||||
@ApiOperation({ summary: "Get schedules of a team member" })
|
||||
async getUserSchedules(
|
||||
@Param("userId", ParseIntPipe) userId: number
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const schedules = await this.schedulesService.getUserSchedules(userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Teams Schedules Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = "org-admin-schedules-teams-controller-e2e@api.com";
|
||||
const userEmail2 = "org-member-schedules-teams-controller-e2e@api.com";
|
||||
const nonOrgUserEmail = "non-org-member-schedules-teams-controller-e2e@api.com";
|
||||
|
||||
const invitedUserEmail = "org-member-invited-schedules-teams-controller-e2e@api.com";
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
let nonOrgUser: User;
|
||||
|
||||
let userToInviteViaApi: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
name: "User 2 schedule",
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
nonOrgUser = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail,
|
||||
username: nonOrgUserEmail,
|
||||
});
|
||||
|
||||
userToInviteViaApi = await userRepositoryFixture.create({
|
||||
email: invitedUserEmail,
|
||||
username: invitedUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
orgTeam = await teamsRepositoryFixture.create({
|
||||
name: "Org Team",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: "Non Org Team",
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: orgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: orgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: userToInviteViaApi.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: nonOrgUser.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user2.id}`,
|
||||
username: userEmail2,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${userToInviteViaApi.id}`,
|
||||
username: invitedUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: userToInviteViaApi.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should get all the schedule of the org's team's member", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/users/${user2.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<ScheduleOutput_2024_06_11[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data.find((d) => d.id === user2Schedule.id)?.name).toEqual(user2Schedule.name);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
await userRepositoryFixture.deleteByEmail(nonOrgUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/contro
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -112,6 +113,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsAttributesController,
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
@@ -103,6 +103,24 @@ export class OrganizationsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgTeamUser(organizationId: number, teamId: number, userId: number) {
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
profiles: {
|
||||
some: {
|
||||
organizationId,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
some: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fetchOrgAdminApiStatus(organizationId: number) {
|
||||
return this.dbRead.prisma.organizationSettings.findUnique({
|
||||
where: {
|
||||
|
||||
+26
@@ -5,6 +5,27 @@ import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, ValidateNested
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class MembershipUserOutputDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly username?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly email!: string;
|
||||
}
|
||||
|
||||
export class OrgTeamMembershipOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@@ -31,6 +52,11 @@ export class OrgTeamMembershipOutputDto {
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly disableImpersonation?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => MembershipUserOutputDto)
|
||||
@Expose()
|
||||
user!: MembershipUserOutputDto;
|
||||
}
|
||||
|
||||
export class OrgTeamMembershipsOutputResponseDto {
|
||||
|
||||
+5
-1
@@ -9,13 +9,14 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async findOrgTeamMembershipsPaginated(organizationId: number, teamId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.membership.findMany({
|
||||
return await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: teamId,
|
||||
team: {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
@@ -30,6 +31,7 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
async deleteOrgTeamMembershipById(organizationId: number, teamId: number, membershipId: number) {
|
||||
@@ -59,12 +61,14 @@ export class OrganizationsTeamsMembershipsRepository {
|
||||
parentId: organizationId,
|
||||
},
|
||||
},
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: teamId },
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2412,6 +2412,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsTeamsSchedulesController_getUserSchedules",
|
||||
"summary": "Get schedules of a team member",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Teams / Schedules",
|
||||
"Orgs / Teams / Users / Schedules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersController_getOrganizationsUsers",
|
||||
@@ -10447,6 +10479,26 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"MembershipUserOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatarUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"OrgTeamMembershipOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10472,6 +10524,9 @@
|
||||
},
|
||||
"disableImpersonation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/MembershipUserOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10479,7 +10534,8 @@
|
||||
"id",
|
||||
"userId",
|
||||
"teamId",
|
||||
"accepted"
|
||||
"accepted",
|
||||
"user"
|
||||
]
|
||||
},
|
||||
"OrgTeamMembershipsOutputResponseDto": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useState } from "react";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/monorepo";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar, Button, Icon, Select, Tooltip } from "@calcom/ui";
|
||||
@@ -30,6 +31,7 @@ export const CheckedTeamSelect = ({
|
||||
onChange: (value: readonly CheckedSelectOption[]) => void;
|
||||
isRRWeightsEnabled?: boolean;
|
||||
}) => {
|
||||
const isPlatform = useIsPlatform();
|
||||
const [priorityDialogOpen, setPriorityDialogOpen] = useState(false);
|
||||
const [weightDialogOpen, setWeightDialogOpen] = useState(false);
|
||||
|
||||
@@ -59,7 +61,8 @@ export const CheckedTeamSelect = ({
|
||||
<li
|
||||
key={option.value}
|
||||
className={`flex px-3 py-2 ${index === value.length - 1 ? "" : "border-subtle border-b"}`}>
|
||||
<Avatar size="sm" imageSrc={option.avatar} alt={option.label} />
|
||||
{!isPlatform && <Avatar size="sm" imageSrc={option.avatar} alt={option.label} />}
|
||||
{isPlatform && <Icon name="user" className="mt-0.5 h-4 w-4" />}
|
||||
<p className="text-emphasis my-auto ms-3 text-sm">{option.label}</p>
|
||||
<div className="ml-auto flex items-center">
|
||||
{option && !option.isFixed ? (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { useState, memo, useEffect } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import type { GetAllSchedulesByUserIdQueryType } from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper";
|
||||
import { useIsPlatform } from "@calcom/atoms/monorepo";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { SelectSkeletonLoader } from "@calcom/features/availability/components/SkeletonLoader";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
@@ -35,18 +37,35 @@ type EventTypeScheduleDetailsProps = {
|
||||
editAvailabilityRedirectUrl?: string;
|
||||
};
|
||||
|
||||
type HostSchedulesQueryType =
|
||||
| GetAllSchedulesByUserIdQueryType
|
||||
| (({ userId }: { userId: number }) => UseQueryResult<
|
||||
{
|
||||
schedules: {
|
||||
id: number;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
userId: number;
|
||||
readOnly: boolean;
|
||||
}[];
|
||||
},
|
||||
Error
|
||||
>);
|
||||
|
||||
type EventTypeTeamScheduleProps = {
|
||||
hostSchedulesQuery: GetAllSchedulesByUserIdQueryType;
|
||||
hostSchedulesQuery: HostSchedulesQueryType;
|
||||
hosts?: Host[];
|
||||
};
|
||||
|
||||
type TeamMember = Pick<TeamMembers[number], "avatar" | "name" | "id">;
|
||||
|
||||
type EventTypeScheduleProps = {
|
||||
schedulesQueryData?: Array<
|
||||
Omit<RouterOutputs["viewer"]["availability"]["list"]["schedules"][number], "availability">
|
||||
>;
|
||||
isSchedulesPending?: boolean;
|
||||
eventType: EventTypeSetup;
|
||||
teamMembers: TeamMembers;
|
||||
teamMembers: TeamMember[];
|
||||
} & EventTypeScheduleDetailsProps &
|
||||
EventTypeTeamScheduleProps;
|
||||
|
||||
@@ -302,10 +321,11 @@ const TeamMemberSchedule = ({
|
||||
}: {
|
||||
host: Host;
|
||||
index: number;
|
||||
teamMembers: TeamMembers;
|
||||
hostScheduleQuery: GetAllSchedulesByUserIdQueryType;
|
||||
teamMembers: TeamMember[];
|
||||
hostScheduleQuery: HostSchedulesQueryType;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { getValues } = formMethods;
|
||||
@@ -337,7 +357,8 @@ const TeamMemberSchedule = ({
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<Avatar size="sm" imageSrc={avatar} alt={label || ""} />
|
||||
{!isPlatform && <Avatar size="sm" imageSrc={avatar} alt={label || ""} />}
|
||||
{isPlatform && <Icon name="user" className="h-4 w-4" />}
|
||||
<p className="text-emphasis my-auto ms-3 text-sm">{label}</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-col pt-2 ">
|
||||
@@ -374,7 +395,7 @@ const TeamAvailability = ({
|
||||
hosts,
|
||||
teamMembers,
|
||||
hostSchedulesQuery,
|
||||
}: EventTypeTeamScheduleProps & { teamMembers: TeamMembers }) => {
|
||||
}: EventTypeTeamScheduleProps & { teamMembers: TeamMember[] }) => {
|
||||
const { t } = useLocale();
|
||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ export function BaseCalProvider({
|
||||
isValidClient: Boolean(!error && clientId && isInit),
|
||||
isAuth: Boolean(isInit && !error && clientId && currentAccessToken && http.getAuthorizationHeader()),
|
||||
organizationId: organizationId || stateOrgId || me?.data.organizationId || 0,
|
||||
userId: me?.data.id,
|
||||
...translations,
|
||||
}}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../hooks/useAtomsContext";
|
||||
import http from "../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-atom-host-schedule-by-id";
|
||||
|
||||
export const useAtomHostSchedules = ({ userId, teamId }: { userId: number; teamId?: number }) => {
|
||||
const { organizationId, userId: meId } = useAtomsContext();
|
||||
|
||||
const event = useQuery({
|
||||
queryKey: [QUERY_KEY, organizationId, userId],
|
||||
queryFn: async () => {
|
||||
const pathname = `/organizations/${organizationId}/teams/${teamId}/users/${userId}/schedules`;
|
||||
return http?.get<GetSchedulesOutput_2024_06_11>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
const schedules = res.data.data.map((schedule) => ({
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
isDefault: schedule.isDefault,
|
||||
userId: schedule.ownerId,
|
||||
readOnly: meId !== userId,
|
||||
}));
|
||||
return { schedules };
|
||||
}
|
||||
throw new Error(res.data?.error?.message ?? "Error while getting host schedules");
|
||||
});
|
||||
},
|
||||
enabled: !!organizationId && !!teamId && !!userId,
|
||||
});
|
||||
|
||||
return event;
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import type { User } from "@calcom/prisma/client";
|
||||
|
||||
import type { Availability } from "../../availability/AvailabilitySettings";
|
||||
import { transformApiScheduleForAtom } from "../../availability/atom-api-transformers/transformApiScheduleForAtom";
|
||||
import { useTeamMembers } from "../../hooks/teams/useTeamMembers";
|
||||
import { useAtomHostSchedules } from "../hooks/useAtomHostSchedules";
|
||||
import { useSchedule } from "../hooks/useSchedule";
|
||||
import { useSchedules } from "../hooks/useSchedules";
|
||||
|
||||
@@ -13,10 +15,12 @@ type EventAvailabilityTabPlatformWrapperProps = {
|
||||
user?: Pick<User, "id" | "defaultScheduleId" | "timeZone" | "timeFormat" | "weekStart">;
|
||||
eventType: EventTypeSetup;
|
||||
isTeamEvent: boolean;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
const EventAvailabilityTabPlatformWrapper = ({
|
||||
user,
|
||||
teamId,
|
||||
...props
|
||||
}: EventAvailabilityTabPlatformWrapperProps) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
@@ -29,6 +33,8 @@ const EventAvailabilityTabPlatformWrapper = ({
|
||||
const { data: schedulesQueryData, isLoading: isSchedulesPending } = useSchedules();
|
||||
|
||||
const atomSchedule = transformApiScheduleForAtom(user, scheduleQueryData, schedulesQueryData?.length || 0);
|
||||
const hostSchedulesQuery = useAtomHostSchedules;
|
||||
const { data: teamMembers } = useTeamMembers({ teamId });
|
||||
|
||||
if (!atomSchedule) {
|
||||
return <></>;
|
||||
@@ -38,9 +44,17 @@ const EventAvailabilityTabPlatformWrapper = ({
|
||||
<EventAvailabilityTab
|
||||
{...props}
|
||||
user={user}
|
||||
teamMembers={
|
||||
teamMembers?.map((member) => ({
|
||||
avatar: member.user.avatarUrl ?? "",
|
||||
id: member.userId,
|
||||
name: member.user.name,
|
||||
})) ?? []
|
||||
}
|
||||
schedulesQueryData={schedulesQueryData}
|
||||
isSchedulesPending={isSchedulesPending}
|
||||
isSchedulePending={isSchedulePending}
|
||||
hostSchedulesQuery={({ userId }: { userId: number }) => hostSchedulesQuery({ userId, teamId })}
|
||||
scheduleQueryData={{
|
||||
isManaged: atomSchedule.isManaged,
|
||||
readOnly: atomSchedule.readOnly,
|
||||
|
||||
@@ -126,7 +126,12 @@ const EventType = ({
|
||||
<></>
|
||||
),
|
||||
availability: tabs.includes("availability") ? (
|
||||
<EventAvailabilityTabPlatformWrapper eventType={eventType} isTeamEvent={!!team} user={user?.data} />
|
||||
<EventAvailabilityTabPlatformWrapper
|
||||
eventType={eventType}
|
||||
isTeamEvent={!!team}
|
||||
user={user?.data}
|
||||
teamId={team?.id}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
import type { Membership, User } from "@calcom/prisma/client";
|
||||
|
||||
import http from "../../lib/http";
|
||||
import { useAtomsContext } from "../useAtomsContext";
|
||||
|
||||
export const QUERY_KEY = "use-team-members";
|
||||
|
||||
type ResponseDataType = Membership & { user: Pick<User, "email" | "name" | "avatarUrl" | "username"> };
|
||||
|
||||
export const useTeamMembers = ({ teamId }: { teamId?: number }) => {
|
||||
const { organizationId } = useAtomsContext();
|
||||
|
||||
const event = useQuery({
|
||||
queryKey: [QUERY_KEY, organizationId, teamId],
|
||||
queryFn: async () => {
|
||||
const pathname = `/organizations/${organizationId}/teams/${teamId}/memberships`;
|
||||
|
||||
return http?.get<ApiResponse<ResponseDataType[]>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ResponseDataType[]>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!organizationId && !!teamId,
|
||||
});
|
||||
|
||||
return event;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export interface IAtomsContext {
|
||||
exists: (key: translationKeys | string) => boolean;
|
||||
};
|
||||
organizationId: number;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
export const AtomsContext = createContext({
|
||||
|
||||
Reference in new issue
Block a user