fix: eventType atom team availability tab (#17158)

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
MorganandSomay Chauhan authored and GitHub committed 2024-10-21 10:59:07 +03:00
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;
}
}
@@ -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);
});
@@ -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,
};
}
}
@@ -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: {
@@ -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 {
@@ -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 } } },
});
}
}
+57 -1
View File
@@ -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({