Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e481f2921 | ||
|
|
0e6203ac3c | ||
|
|
624d6d7001 | ||
|
|
ec03ab632e | ||
|
|
c81b8145b5 | ||
|
|
fa35cc5210 | ||
|
|
1eb6ff3e65 | ||
|
|
fc4ecf500a | ||
|
|
9d4522825b | ||
|
|
e91bf53d80 | ||
|
|
79b319c1dd | ||
|
|
9197a73b31 | ||
|
|
ec3656e646 | ||
|
|
ae4b807500 | ||
|
|
34a9872df1 | ||
|
|
91a480b83a | ||
|
|
db19bb3d51 | ||
|
|
9afdb1ad7e | ||
|
|
78c2a69389 | ||
|
|
5a483e75c9 | ||
|
|
bb9c4bb8f3 | ||
|
|
02fdc1361c | ||
|
|
573132198e | ||
|
|
9133bc5b16 | ||
|
|
94ae3a5a73 | ||
|
|
0757b00db7 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@calcom/atoms": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
This PR adds ability to display bookings of a user event for calendar view atom
|
|
||||||
+4
-4
@@ -24,10 +24,10 @@
|
|||||||
/packages/embeds/**/* @calcom/Foundation
|
/packages/embeds/**/* @calcom/Foundation
|
||||||
/packages/features/auth/lib/next-auth-options.ts @calcom/Foundation
|
/packages/features/auth/lib/next-auth-options.ts @calcom/Foundation
|
||||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||||
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
/packages/features/availability/lib/getUserAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
/packages/features/bookings/lib/getLuckyUser.ts @calcom/Foundation
|
||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/features/schedules/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
/packages/platform/atoms/**/*WebWrapper.tsx @calcom/Consumer
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/Foundation
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
@@ -1,10 +1,10 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||||
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ import {
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import type {
|
||||||
|
GetAvailabilityListHandlerReturn,
|
||||||
|
DuplicateScheduleHandlerReturn,
|
||||||
|
} from "@calcom/platform-libraries/schedules";
|
||||||
|
import { getAvailabilityListHandler, duplicateScheduleHandler } from "@calcom/platform-libraries/schedules";
|
||||||
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
|
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
|
||||||
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
|
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
|
||||||
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
|
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
|
||||||
@@ -68,6 +73,21 @@ export class AtomsSchedulesController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/schedules/all")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
async getAllUserSchedules(
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<ApiResponse<Awaited<GetAvailabilityListHandlerReturn>>> {
|
||||||
|
const userSchedules = await getAvailabilityListHandler({ ctx: { user } });
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: userSchedules,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Patch("schedules/:scheduleId")
|
@Patch("schedules/:scheduleId")
|
||||||
@Permissions([SCHEDULE_WRITE])
|
@Permissions([SCHEDULE_WRITE])
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
@@ -104,4 +124,20 @@ export class AtomsSchedulesController {
|
|||||||
data: createdSchedule,
|
data: createdSchedule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("schedules/:scheduleId/duplicate")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiOperation({ summary: "Duplicate existing schedule" })
|
||||||
|
async duplicateExistingSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("scheduleId", ParseIntPipe) scheduleId: number
|
||||||
|
): Promise<ApiResponse<Awaited<DuplicateScheduleHandlerReturn>>> {
|
||||||
|
const duplicatedSchedule = await duplicateScheduleHandler({ ctx: { user }, input: { scheduleId } });
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: duplicatedSchedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Locales } from "@/lib/enums/locales";
|
import { Locales } from "@/lib/enums/locales";
|
||||||
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { Transform } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
import { IsOptional, IsTimeZone, IsString, IsEnum, IsIn, IsUrl, IsObject } from "class-validator";
|
import { IsOptional, IsTimeZone, IsString, IsEnum, IsIn, IsUrl, IsObject, IsNumber } from "class-validator";
|
||||||
|
|
||||||
import { ValidateMetadata } from "@calcom/platform-types";
|
import { ValidateMetadata } from "@calcom/platform-types";
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ export class CreateManagedUserInput {
|
|||||||
@ApiProperty({ example: "Alice Smith", description: "Managed user's name is used in emails" })
|
@ApiProperty({ example: "Alice Smith", description: "Managed user's name is used in emails" })
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn([12, 24], { message: "timeFormat must be a number either 12 or 24" })
|
@IsIn([12, 24], { message: "timeFormat must be a number either 12 or 24" })
|
||||||
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be a number 12 or 24" })
|
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be a number 12 or 24" })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Locales } from "@/lib/enums/locales";
|
|||||||
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
||||||
import { TimeFormat, WeekDay } from "@/modules/users/inputs/create-managed-user.input";
|
import { TimeFormat, WeekDay } from "@/modules/users/inputs/create-managed-user.input";
|
||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform, Type } from "class-transformer";
|
||||||
import { IsEnum, IsIn, IsNumber, IsObject, IsOptional, IsString, IsTimeZone, IsUrl } from "class-validator";
|
import { IsEnum, IsIn, IsNumber, IsObject, IsOptional, IsString, IsTimeZone, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import { ValidateMetadata } from "@calcom/platform-types";
|
import { ValidateMetadata } from "@calcom/platform-types";
|
||||||
@@ -17,6 +18,8 @@ export class UpdateManagedUserInput {
|
|||||||
@ApiPropertyOptional()
|
@ApiPropertyOptional()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn([12, 24])
|
@IsIn([12, 24])
|
||||||
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventTy
|
|||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|||||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||||
|
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
|
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
|
||||||
|
|
||||||
@@ -56,15 +57,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
|||||||
// This is because the data is cached and as a result the data is converted to a string
|
// This is because the data is cached and as a result the data is converted to a string
|
||||||
const availabilities = {
|
const availabilities = {
|
||||||
...cachedAvailabilities,
|
...cachedAvailabilities,
|
||||||
schedules: cachedAvailabilities.schedules.map((schedule) => ({
|
schedules: cachedAvailabilities.schedules.map((schedule) => getScheduleListItemData(schedule)),
|
||||||
...schedule,
|
|
||||||
availability: schedule.availability.map((avail) => ({
|
|
||||||
...avail,
|
|
||||||
startTime: new Date(avail.startTime),
|
|
||||||
endTime: new Date(avail.endTime),
|
|
||||||
date: avail.date ? new Date(avail.date) : null,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
|
const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { unstable_cache } from "next/cache";
|
|||||||
|
|
||||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|||||||
@@ -53,16 +53,23 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
|||||||
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
|
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const isOrgAdminOrOwner = checkAdminOrOwner(session.user.org?.role);
|
const isOrgAdminOrOwner = checkAdminOrOwner(session.user.org?.role);
|
||||||
const [features, rolePermissions, organizationPermissions] = await Promise.all([
|
const features = await getTeamFeatures(orgId);
|
||||||
getTeamFeatures(orgId),
|
|
||||||
getCachedResourcePermissions(userId, orgId, Resource.Role),
|
|
||||||
getCachedResourcePermissions(userId, orgId, Resource.Organization),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (features) {
|
if (features) {
|
||||||
teamFeatures = {
|
teamFeatures = {
|
||||||
[orgId]: features,
|
[orgId]: features,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if PBAC feature is enabled
|
||||||
|
const isPbacEnabled = features?.pbac === true;
|
||||||
|
|
||||||
|
if (isPbacEnabled) {
|
||||||
|
// Only fetch and apply PBAC permissions if the feature is enabled
|
||||||
|
const [rolePermissions, organizationPermissions] = await Promise.all([
|
||||||
|
getCachedResourcePermissions(userId, orgId, Resource.Role),
|
||||||
|
getCachedResourcePermissions(userId, orgId, Resource.Organization),
|
||||||
|
]);
|
||||||
|
|
||||||
// Check if user has permission to read roles
|
// Check if user has permission to read roles
|
||||||
const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role);
|
const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role);
|
||||||
@@ -71,7 +78,8 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
|
|||||||
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
|
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
|
||||||
canUpdateOrganization = orgActions[CrudAction.Update] ?? isOrgAdminOrOwner;
|
canUpdateOrganization = orgActions[CrudAction.Update] ?? isOrgAdminOrOwner;
|
||||||
} else {
|
} else {
|
||||||
canViewRoles = false;
|
// Fall back to legacy permissions when PBAC is not enabled or features not loaded
|
||||||
|
canViewRoles = features ? isOrgAdminOrOwner : false;
|
||||||
canViewOrganizationBilling = isOrgAdminOrOwner;
|
canViewOrganizationBilling = isOrgAdminOrOwner;
|
||||||
canUpdateOrganization = isOrgAdminOrOwner;
|
canUpdateOrganization = isOrgAdminOrOwner;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
|
||||||
|
import ResumeOnboardingPage, { LayoutWrapper } from "~/settings/organizations/new/resume-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("resume_onboarding"),
|
||||||
|
(t) => t("resume_onboarding_description"),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"/settings/organizations/new/resume"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ServerPage = async () => {
|
||||||
|
return (
|
||||||
|
<LayoutWrapper>
|
||||||
|
<ResumeOnboardingPage />
|
||||||
|
</LayoutWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerPage;
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
|
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
|
||||||
import { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
|
|
||||||
|
|
||||||
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
|
import { defaultResponderForAppDir } from "../../defaultResponderForAppDir";
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { findUniqueDelegationCalendarCredential } from "@calcom/app-store/delega
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||||
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||||
import type { Ensure } from "@calcom/types/utils";
|
import type { Ensure } from "@calcom/types/utils";
|
||||||
|
|||||||
@@ -160,13 +160,21 @@ export const AppPage = ({
|
|||||||
function refactorMeWithoutEffect() {
|
function refactorMeWithoutEffect() {
|
||||||
const data = appDbQuery.data;
|
const data = appDbQuery.data;
|
||||||
|
|
||||||
const credentialsCount = data?.credentials.length || 0;
|
const credentials = data?.credentials || [];
|
||||||
setExistingCredentials(data?.credentials || []);
|
setExistingCredentials(credentials);
|
||||||
|
|
||||||
|
const hasPersonalInstall = credentials.some((c) => !!c.userId && !c.teamId);
|
||||||
|
const installedTeamIds = new Set<number>();
|
||||||
|
for (const cred of credentials) {
|
||||||
|
if (cred.teamId) installedTeamIds.add(cred.teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalInstalledTargets = (hasPersonalInstall ? 1 : 0) + installedTeamIds.size;
|
||||||
|
|
||||||
const appInstalledForAllTargets =
|
const appInstalledForAllTargets =
|
||||||
availableForTeams && data?.userAdminTeams && data.userAdminTeams.length > 0
|
availableForTeams && data?.userAdminTeams && data.userAdminTeams.length > 0
|
||||||
? credentialsCount >= data.userAdminTeams.length
|
? totalInstalledTargets >= data.userAdminTeams.length + 1
|
||||||
: credentialsCount > 0;
|
: credentials.length > 0;
|
||||||
setAppInstalledForAllTargets(appInstalledForAllTargets);
|
setAppInstalledForAllTargets(appInstalledForAllTargets);
|
||||||
},
|
},
|
||||||
[appDbQuery.data, availableForTeams]
|
[appDbQuery.data, availableForTeams]
|
||||||
@@ -244,7 +252,14 @@ export const AppPage = ({
|
|||||||
loading: isLoading,
|
loading: isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return <InstallAppButtonChild credentials={appDbQuery.data?.credentials} paid={paid} {...props} />;
|
|
||||||
|
return (
|
||||||
|
<InstallAppButtonChild
|
||||||
|
credentials={availableForTeams ? undefined : appDbQuery.data?.credentials}
|
||||||
|
paid={paid}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,12 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
props: {
|
props: {
|
||||||
isEmbed,
|
isEmbed,
|
||||||
profile: {
|
profile: {
|
||||||
theme: form.user.theme,
|
theme: formWithUserProfile.user.profile?.organization?.theme ?? formWithUserProfile.user.theme,
|
||||||
brandColor: form.user.brandColor,
|
brandColor:
|
||||||
darkBrandColor: form.user.darkBrandColor,
|
formWithUserProfile.user.profile?.organization?.brandColor ?? formWithUserProfile.user.brandColor,
|
||||||
|
darkBrandColor:
|
||||||
|
formWithUserProfile.user.profile?.organization?.darkBrandColor ??
|
||||||
|
formWithUserProfile.user.darkBrandColor,
|
||||||
},
|
},
|
||||||
form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }),
|
form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }),
|
||||||
},
|
},
|
||||||
|
|||||||
+15
-3
@@ -49,6 +49,13 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
profile: {
|
profile: {
|
||||||
select: {
|
select: {
|
||||||
organizationId: true,
|
organizationId: true,
|
||||||
|
organization: {
|
||||||
|
select: {
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
theme: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teamId: true,
|
teamId: true,
|
||||||
@@ -71,9 +78,15 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
name: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
theme: true,
|
||||||
parent: {
|
parent: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
theme: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdByOAuthClientId: true,
|
createdByOAuthClientId: true,
|
||||||
@@ -116,12 +129,11 @@ export const getEventTypesFromDB = async (id: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||||
const { profile, ...restEventType } = eventType;
|
const isOrgTeamEvent = !!eventType?.team && !!eventType.profile?.organizationId;
|
||||||
const isOrgTeamEvent = !!eventType?.team && !!profile?.organizationId;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
...restEventType,
|
...eventType,
|
||||||
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
|
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
|||||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
|
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
|
import { HashedLinkService } from "@calcom/features/hashedLink/services/hashedLinkService";
|
||||||
import {
|
import {
|
||||||
shouldHideBrandingForTeamEvent,
|
shouldHideBrandingForTeamEvent,
|
||||||
shouldHideBrandingForUserEvent,
|
shouldHideBrandingForUserEvent,
|
||||||
} from "@calcom/features/profile/lib/hideBranding";
|
} from "@calcom/features/profile/lib/hideBranding";
|
||||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
import { HashedLinkService } from "@calcom/lib/server/service/hashedLinkService";
|
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { RedirectType } from "@calcom/prisma/enums";
|
import { RedirectType } from "@calcom/prisma/enums";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { organizationScenarios } from "@calcom/lib/server/repository/__mocks__/organization";
|
import { organizationScenarios } from "@calcom/features/ee/organizations/__mocks__/organizationMock";
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { URLSearchParams } from "url";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { buildEventUrlFromBooking } from "@calcom/features/bookings/lib/buildEventUrlFromBooking";
|
||||||
import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect";
|
import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect";
|
||||||
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||||
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
|
|
||||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
|
||||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booki
|
|||||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
|
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
|
||||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getBrandingForEventType } from "@calcom/features/profile/lib/getBranding";
|
||||||
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
|
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
|
||||||
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
@@ -30,7 +31,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const { req, params, query } = context;
|
const { req, params, query } = context;
|
||||||
const session = await getServerSession({ req });
|
const session = await getServerSession({ req });
|
||||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
|
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
|
||||||
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query;
|
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting } = query;
|
||||||
const allowRescheduleForCancelledBooking = query.allowRescheduleForCancelledBooking === "true";
|
const allowRescheduleForCancelledBooking = query.allowRescheduleForCancelledBooking === "true";
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
|
||||||
|
|
||||||
@@ -133,6 +134,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
|
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
|
||||||
const useApiV2 = teamHasApiV2Route && hasApiV2RouteInEnv();
|
const useApiV2 = teamHasApiV2Route && hasApiV2RouteInEnv();
|
||||||
|
|
||||||
|
const branding = getBrandingForEventType({
|
||||||
|
eventType: {
|
||||||
|
team: team.parent ?? team,
|
||||||
|
users: [],
|
||||||
|
profile: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
useApiV2,
|
useApiV2,
|
||||||
@@ -153,6 +162,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
: getPlaceholderAvatar(team.logoUrl, team.name),
|
: getPlaceholderAvatar(team.logoUrl, team.name),
|
||||||
name,
|
name,
|
||||||
username: orgSlug ?? null,
|
username: orgSlug ?? null,
|
||||||
|
...branding,
|
||||||
},
|
},
|
||||||
title: eventData.title,
|
title: eventData.title,
|
||||||
users: eventHostsUserData,
|
users: eventHostsUserData,
|
||||||
@@ -204,6 +214,9 @@ const getTeamWithEventsData = async (
|
|||||||
bannerUrl: true,
|
bannerUrl: true,
|
||||||
logoUrl: true,
|
logoUrl: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
theme: true,
|
||||||
organizationSettings: {
|
organizationSettings: {
|
||||||
select: {
|
select: {
|
||||||
allowSEOIndexing: true,
|
allowSEOIndexing: true,
|
||||||
@@ -214,6 +227,9 @@ const getTeamWithEventsData = async (
|
|||||||
logoUrl: true,
|
logoUrl: true,
|
||||||
name: true,
|
name: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
theme: true,
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
where: {
|
where: {
|
||||||
slug: meetingSlug,
|
slug: meetingSlug,
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
|||||||
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
|
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
|
||||||
{availabilities.schedules.map((schedule) => (
|
{availabilities.schedules.map((schedule) => (
|
||||||
<ScheduleListItem
|
<ScheduleListItem
|
||||||
|
redirectUrl={`/availability/${schedule.id}`}
|
||||||
displayOptions={{
|
displayOptions={{
|
||||||
hour12: user?.timeFormat ? user.timeFormat === 12 : undefined,
|
hour12: user?.timeFormat ? user.timeFormat === 12 : undefined,
|
||||||
timeZone: user?.timeZone,
|
timeZone: user?.timeZone,
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-util
|
|||||||
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||||
|
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||||
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||||
|
import { getBrandingForEventType } from "@calcom/features/profile/lib/getBranding";
|
||||||
import { shouldHideBrandingForEvent } from "@calcom/features/profile/lib/hideBranding";
|
import { shouldHideBrandingForEvent } from "@calcom/features/profile/lib/hideBranding";
|
||||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { customInputSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||||
@@ -115,7 +116,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
|
||||||
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
eventTypeRaw.users = eventTypeRaw.hosts?.length
|
||||||
? eventTypeRaw.hosts.map((host) => host.user)
|
? eventTypeRaw.hosts.map((host) => host.user)
|
||||||
: eventTypeRaw.users;
|
: eventTypeRaw.users;
|
||||||
|
|
||||||
@@ -150,9 +151,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
const profile = {
|
const profile = {
|
||||||
name: eventType.team?.name || eventType.users[0]?.name || null,
|
name: eventType.team?.name || eventType.users[0]?.name || null,
|
||||||
email: eventType.team ? null : eventType.users[0].email || null,
|
email: eventType.team ? null : eventType.users[0].email || null,
|
||||||
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
|
...getBrandingForEventType({ eventType: eventTypeRaw }),
|
||||||
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
|
|
||||||
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
|
|
||||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ import {
|
|||||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||||
import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks";
|
import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks";
|
||||||
import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating";
|
import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating";
|
||||||
|
import {
|
||||||
|
SMS_REMINDER_NUMBER_FIELD,
|
||||||
|
SystemField,
|
||||||
|
TITLE_FIELD,
|
||||||
|
} from "@calcom/lib/bookings/SystemField";
|
||||||
import type { nameObjectSchema } from "@calcom/features/eventtypes/lib/eventNaming";
|
import type { nameObjectSchema } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||||
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField, TITLE_FIELD } from "@calcom/lib/SystemField";
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
|
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
|
||||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
@@ -694,7 +698,7 @@ export default function Success(props: PageProps) {
|
|||||||
<>
|
<>
|
||||||
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
||||||
<div className="col-span-2 mb-2 mt-9">
|
<div className="col-span-2 mb-2 mt-9">
|
||||||
<p className="break-words">{bookingInfo.description}</p>
|
<p className="whitespace-pre-line break-words">{bookingInfo.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -767,7 +771,7 @@ export default function Success(props: PageProps) {
|
|||||||
<Fragment key={field.name}>
|
<Fragment key={field.name}>
|
||||||
<div
|
<div
|
||||||
className="text-emphasis mt-4 font-medium"
|
className="text-emphasis mt-4 font-medium"
|
||||||
// eslint-disable-next-line react/no-danger
|
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: markdownToSafeHTML(label),
|
__html: markdownToSafeHTML(label),
|
||||||
}}
|
}}
|
||||||
@@ -1080,7 +1084,7 @@ export default function Success(props: PageProps) {
|
|||||||
<span className="underline">
|
<span className="underline">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations">
|
href="https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations" rel="noreferrer">
|
||||||
{t("resolve")}
|
{t("resolve")}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ const AddNewTeamsFormChild = ({ teams }: { teams: { id: number; name: string; sl
|
|||||||
<TextField
|
<TextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
{...register(`teams.${index}.name`)}
|
{...register(`teams.${index}.name`)}
|
||||||
|
data-testid={`team.${index}.name`}
|
||||||
label=""
|
label=""
|
||||||
addOnClassname="bg-transparent p-0 border-l-0"
|
addOnClassname="bg-transparent p-0 border-l-0"
|
||||||
className={index > 0 ? "mb-2" : ""}
|
className={index > 0 ? "mb-2" : ""}
|
||||||
|
|||||||
@@ -34,28 +34,28 @@ const useOrgCreation = () => {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const [serverErrorMessage, setServerErrorMessage] = useState("");
|
const [serverErrorMessage, setServerErrorMessage] = useState("");
|
||||||
const { useOnboardingStore, isBillingEnabled } = useOnboarding();
|
const { useOnboardingStore } = useOnboarding();
|
||||||
const { reset } = useOnboardingStore();
|
const { reset } = useOnboardingStore();
|
||||||
|
|
||||||
const checkoutMutation = trpc.viewer.organizations.createWithPaymentIntent.useMutation({
|
// Single mutation for all flows (billing, self-hosted, admin)
|
||||||
onSuccess: (data) => {
|
const intentToCreateOrgMutation = trpc.viewer.organizations.intentToCreateOrg.useMutation({
|
||||||
if (data.checkoutUrl) {
|
|
||||||
window.location.href = data.checkoutUrl;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setServerErrorMessage(t(error.message));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createOrgMutation = trpc.viewer.organizations.createSelfHosted.useMutation({
|
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
if (data.organization) {
|
reset({
|
||||||
// Invalidate the organizations query to ensure fresh data on the next page
|
onboardingId: data.organizationOnboardingId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.checkoutUrl) {
|
||||||
|
// Billing enabled - redirect to Stripe
|
||||||
|
window.location.href = data.checkoutUrl;
|
||||||
|
} else if (data.organizationId) {
|
||||||
|
// Self-hosted - org already created, redirect to organizations
|
||||||
await utils.viewer.organizations.listCurrent.invalidate();
|
await utils.viewer.organizations.listCurrent.invalidate();
|
||||||
await session.update();
|
await session.update();
|
||||||
reset();
|
reset();
|
||||||
window.location.href = `${window.location.origin}/settings/organizations/profile`;
|
window.location.href = `${window.location.origin}/settings/organizations/profile`;
|
||||||
|
} else {
|
||||||
|
// Unexpected state
|
||||||
|
setServerErrorMessage("Unexpected response from server");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -63,12 +63,10 @@ const useOrgCreation = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutationToUse = isBillingEnabled ? checkoutMutation : createOrgMutation;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: mutationToUse,
|
mutation: intentToCreateOrgMutation,
|
||||||
mutate: mutationToUse.mutate,
|
mutate: intentToCreateOrgMutation.mutate,
|
||||||
isPending: mutationToUse.isPending,
|
isPending: intentToCreateOrgMutation.isPending,
|
||||||
errorMessage: serverErrorMessage,
|
errorMessage: serverErrorMessage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -84,7 +82,13 @@ export const AddNewTeamMembersForm = () => {
|
|||||||
invitedMembers,
|
invitedMembers,
|
||||||
logo,
|
logo,
|
||||||
bio,
|
bio,
|
||||||
onboardingId,
|
name,
|
||||||
|
slug,
|
||||||
|
billingPeriod,
|
||||||
|
seats,
|
||||||
|
pricePerSeat,
|
||||||
|
brandColor,
|
||||||
|
bannerUrl,
|
||||||
} = useOnboardingStore();
|
} = useOnboardingStore();
|
||||||
const orgCreation = useOrgCreation();
|
const orgCreation = useOrgCreation();
|
||||||
|
|
||||||
@@ -155,7 +159,7 @@ export const AddNewTeamMembersForm = () => {
|
|||||||
placeholder="colleague@company.com"
|
placeholder="colleague@company.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" StartIcon="plus" color="secondary">
|
<Button type="submit" StartIcon="plus" color="secondary" data-testid="invite-new-member-button">
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -203,22 +207,31 @@ export const AddNewTeamMembersForm = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 mt-6 flex items-center justify-end">
|
<div className="mt-3 flex items-center justify-end">
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="publish-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!onboardingId) {
|
// Submit ALL data to intentToCreateOrg
|
||||||
console.error("Org owner email and onboardingId are required", {
|
if (!name || !slug || !orgOwnerEmail) {
|
||||||
orgOwnerEmail,
|
console.error("Required fields missing", { name, slug, orgOwnerEmail });
|
||||||
onboardingId,
|
showToast(t("required_fields_missing"), "error");
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
orgCreation.mutation.mutate({
|
|
||||||
|
orgCreation.mutate({
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
orgOwnerEmail,
|
||||||
|
seats,
|
||||||
|
pricePerSeat,
|
||||||
|
billingPeriod,
|
||||||
|
creationSource: "WEBAPP" as const,
|
||||||
logo,
|
logo,
|
||||||
bio,
|
bio,
|
||||||
|
brandColor,
|
||||||
|
bannerUrl,
|
||||||
teams,
|
teams,
|
||||||
invitedMembers,
|
invitedMembers,
|
||||||
onboardingId,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
loading={orgCreation.isPending}>
|
loading={orgCreation.isPending}>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useEffect, useState } from "react";
|
|||||||
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
|
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Icon } from "@calcom/ui/components/icon";
|
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
import { Icon } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const PaymentStatusView = () => {
|
const PaymentStatusView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -15,9 +15,7 @@ const PaymentStatusView = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const paymentStatus = searchParams?.get("paymentStatus");
|
const paymentStatus = searchParams?.get("paymentStatus");
|
||||||
const paymentError = searchParams?.get("error");
|
const paymentError = searchParams?.get("error");
|
||||||
const { useOnboardingStore } = useOnboarding({
|
const { useOnboardingStore } = useOnboarding();
|
||||||
step: "status",
|
|
||||||
});
|
|
||||||
const [organizationCreated, setOrganizationCreated] = useState<boolean>(false);
|
const [organizationCreated, setOrganizationCreated] = useState<boolean>(false);
|
||||||
const { name } = useOnboardingStore();
|
const { name } = useOnboardingStore();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Alert } from "@calcom/ui/components/alert";
|
||||||
|
import { WizardLayout } from "@calcom/ui/components/layout";
|
||||||
|
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||||
|
|
||||||
|
export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout currentStep={1} maxSteps={5}>
|
||||||
|
{children}
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResumeOnboardingView = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const onboardingIdParam = searchParams?.get("onboardingId");
|
||||||
|
|
||||||
|
const { dbOnboarding, isLoadingOrgOnboarding, useOnboardingStore } = useOnboarding();
|
||||||
|
const { reset } = useOnboardingStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoadingOrgOnboarding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onboardingIdParam) {
|
||||||
|
router.push("/settings/organizations/new");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbOnboarding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbOnboarding.isComplete) {
|
||||||
|
router.push("/settings/organizations");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load onboarding data into store
|
||||||
|
reset({
|
||||||
|
onboardingId: dbOnboarding.id,
|
||||||
|
name: dbOnboarding.name,
|
||||||
|
slug: dbOnboarding.slug,
|
||||||
|
orgOwnerEmail: dbOnboarding.orgOwnerEmail,
|
||||||
|
billingPeriod: dbOnboarding.billingPeriod,
|
||||||
|
seats: dbOnboarding.seats,
|
||||||
|
pricePerSeat: dbOnboarding.pricePerSeat,
|
||||||
|
logo: dbOnboarding.logo,
|
||||||
|
bio: dbOnboarding.bio,
|
||||||
|
brandColor: dbOnboarding.brandColor,
|
||||||
|
bannerUrl: dbOnboarding.bannerUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect to next step (About page)
|
||||||
|
router.push("/settings/organizations/new/about");
|
||||||
|
}, [dbOnboarding, isLoadingOrgOnboarding, onboardingIdParam, reset, router]);
|
||||||
|
|
||||||
|
if (isLoadingOrgOnboarding) {
|
||||||
|
return (
|
||||||
|
<SkeletonContainer className="space-y-4">
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
<SkeletonText className="h-4 w-3/4" />
|
||||||
|
<SkeletonText className="h-4 w-1/2" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onboardingIdParam) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
data-testid="error"
|
||||||
|
severity="error"
|
||||||
|
title={t("error")}
|
||||||
|
message={t("no_onboarding_id_provided")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbOnboarding) {
|
||||||
|
return (
|
||||||
|
<Alert data-testid="error" severity="error" title={t("error")} message={t("onboarding_not_found")} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbOnboarding.isComplete) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
data-testid="error"
|
||||||
|
severity="info"
|
||||||
|
title={t("onboarding_already_complete")}
|
||||||
|
message={t("onboarding_already_complete_description")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading state while redirecting
|
||||||
|
return (
|
||||||
|
<SkeletonContainer className="space-y-4">
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
<SkeletonText className="h-4 w-3/4" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResumeOnboardingView;
|
||||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/navigation";
|
|||||||
|
|
||||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
|
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
|
||||||
|
import { useCreateEventType } from "@calcom/features/eventtypes/hooks/useCreateEventType";
|
||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui/components/button";
|
import { Button } from "@calcom/ui/components/button";
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ vi.mock("@calcom/features/bookings/components/event-meta/Price", () => {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@calcom/lib/SystemField", () => {
|
vi.mock("@calcom/lib/bookings/SystemField", () => {
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ function mockedUserPageComponentProps(props: Partial<React.ComponentProps<typeof
|
|||||||
theme: "dark",
|
theme: "dark",
|
||||||
brandColor: "red",
|
brandColor: "red",
|
||||||
darkBrandColor: "black",
|
darkBrandColor: "black",
|
||||||
organization: { requestedSlug: "slug", slug: "slug", id: 1 },
|
organization: {
|
||||||
|
requestedSlug: "slug",
|
||||||
|
slug: "slug",
|
||||||
|
id: 1,
|
||||||
|
brandColor: null,
|
||||||
|
darkBrandColor: null,
|
||||||
|
theme: null,
|
||||||
|
},
|
||||||
allowSEOIndexing: true,
|
allowSEOIndexing: true,
|
||||||
username: "john",
|
username: "john",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.8.0",
|
"version": "5.8.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { ErrorProps } from "next/error";
|
|||||||
import NextError from "next/error";
|
import NextError from "next/error";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
|||||||
@@ -541,9 +541,10 @@ test.describe("Reschedule for booking with seats", () => {
|
|||||||
data: bookingSeats,
|
data: bookingSeats,
|
||||||
});
|
});
|
||||||
|
|
||||||
const references = await prisma.bookingSeat.findMany({
|
const secondAttendeeId = bookingAttendees.find((attendee)=> attendee.email === "second+seats@cal.com");
|
||||||
where: { bookingId: booking.id },
|
|
||||||
orderBy: { id: "asc" },
|
const references = await prisma.bookingSeat.findFirst({
|
||||||
|
where: { bookingId: booking.id, attendeeId: secondAttendeeId?.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
const secondUser = await users.create({
|
const secondUser = await users.create({
|
||||||
@@ -556,13 +557,14 @@ test.describe("Reschedule for booking with seats", () => {
|
|||||||
await page.waitForSelector('[data-testid="bookings"]');
|
await page.waitForSelector('[data-testid="bookings"]');
|
||||||
|
|
||||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||||
|
const href = await page.locator('[data-testid="reschedule"]').getAttribute("href");
|
||||||
|
expect(href).toBeTruthy();
|
||||||
|
const url = new URL(href!, page.url());
|
||||||
|
const seatReferenceUid = url.searchParams.get('seatReferenceUid');
|
||||||
|
expect(seatReferenceUid).toBeTruthy();
|
||||||
|
expect(seatReferenceUid).toBe(references?.referenceUid);
|
||||||
await page.locator('[data-testid="reschedule"]').click();
|
await page.locator('[data-testid="reschedule"]').click();
|
||||||
|
|
||||||
await page.waitForURL((url) => {
|
|
||||||
const rescheduleUid = url.searchParams.get("rescheduleUid");
|
|
||||||
return !!rescheduleUid && rescheduleUid === references[1].referenceUid;
|
|
||||||
});
|
|
||||||
|
|
||||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
const nameElement = page.locator("input[name=name]");
|
const nameElement = page.locator("input[name=name]");
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export function createAppsFixture(page: Page) {
|
|||||||
},
|
},
|
||||||
goToAppsTab: async () => {
|
goToAppsTab: async () => {
|
||||||
await page.getByTestId("vertical-tab-apps").click();
|
await page.getByTestId("vertical-tab-apps").click();
|
||||||
|
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page");
|
||||||
},
|
},
|
||||||
activeApp: async (app: string) => {
|
activeApp: async (app: string) => {
|
||||||
await page.locator(`[data-testid='${app}-app-switch']`).click();
|
await page.locator(`[data-testid='${app}-app-switch']`).click();
|
||||||
@@ -126,7 +127,7 @@ export function createAppsFixture(page: Page) {
|
|||||||
},
|
},
|
||||||
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
|
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
|
||||||
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
|
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
|
||||||
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); // fix the race condition
|
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page"); // fix the race condition
|
||||||
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
|
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { uuid } from "short-uuid";
|
||||||
|
|
||||||
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to complete the organization onboarding wizard steps
|
||||||
|
* (about, add-teams, onboard-members)
|
||||||
|
*/
|
||||||
|
async function completeOnboardingWizard(
|
||||||
|
page: Page,
|
||||||
|
options: {
|
||||||
|
aboutText?: string;
|
||||||
|
teams?: string[];
|
||||||
|
members?: string[];
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const { aboutText = "Test organization description", teams = ["Engineering"], members = [] } = options;
|
||||||
|
|
||||||
|
// Step 1: Complete "About" step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/about");
|
||||||
|
await page.locator('textarea[name="about"]').fill(aboutText);
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Step 2: Complete "Add Teams" step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/add-teams");
|
||||||
|
for (let i = 0; i < teams.length; i++) {
|
||||||
|
await page.getByTestId(`team.${i}.name`).fill(teams[i]);
|
||||||
|
if (i < teams.length - 1) {
|
||||||
|
await page.getByTestId("add_a_team").click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Step 3: Complete "Onboard Members" step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/onboard-members");
|
||||||
|
|
||||||
|
// Add members if provided
|
||||||
|
for (const memberEmail of members) {
|
||||||
|
await page.locator('[placeholder="colleague\\@company\\.com"]').fill(memberEmail);
|
||||||
|
await page.getByTestId("invite-new-member-button").click();
|
||||||
|
await expect(page.getByTestId("pending-member-list")).toContainText(memberEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click publish and wait for response
|
||||||
|
const responsePromise = page.waitForResponse("**/api/trpc/organizations/intentToCreateOrg**");
|
||||||
|
await page.getByTestId("publish-button").click();
|
||||||
|
|
||||||
|
const response = await responsePromise;
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
// TRPC batch response is an array with one element
|
||||||
|
const result = responseData[0].result.data.json;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Organization Creation Flows - Comprehensive Suite", () => {
|
||||||
|
test.afterEach(({ users, orgs }) => {
|
||||||
|
users.deleteAll();
|
||||||
|
orgs.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Admin Handover Flow (Billing Disabled)", () => {
|
||||||
|
test("Admin creates org for existing user - handover and complete", async ({ page, users }) => {
|
||||||
|
// Setup: Create admin and future owner
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
const ownerEmail = users.trackEmail({
|
||||||
|
username: "orgowner",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
const ownerUser = await users.create({
|
||||||
|
username: "orgowner",
|
||||||
|
email: ownerEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgName = "Test Organization";
|
||||||
|
const orgSlug = `test-org-${uuid()}`.toLowerCase();
|
||||||
|
|
||||||
|
// Step 1: Admin fills form
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
await page.locator("input[name=orgOwnerEmail]").fill(ownerEmail);
|
||||||
|
await page.locator("input[name=name]").fill(orgName);
|
||||||
|
await page.locator("input[name=slug]").fill(orgSlug);
|
||||||
|
|
||||||
|
// If billing enabled, admin sees billing fields but we're testing billing disabled scenario
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
await page.locator("input[name=seats]").fill("10");
|
||||||
|
await page.locator("input[name=pricePerSeat]").fill("15");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit and wait for intentToCreateOrg
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse("**/api/trpc/organizations/intentToCreateOrg**"),
|
||||||
|
page.locator("button[type=submit]").click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step 2: Verify admin is on handover page
|
||||||
|
await page.waitForURL("**/settings/organizations/new/handover");
|
||||||
|
|
||||||
|
const onboardingUrlElement = page.getByTestId("onboarding-url");
|
||||||
|
await expect(onboardingUrlElement).toBeVisible();
|
||||||
|
|
||||||
|
const onboardingUrl = await onboardingUrlElement.textContent();
|
||||||
|
expect(onboardingUrl).toContain("/settings/organizations/new/resume?onboardingId=");
|
||||||
|
|
||||||
|
// Extract onboardingId from URL
|
||||||
|
const onboardingIdMatch = onboardingUrl?.match(/onboardingId=([\w-]+)/);
|
||||||
|
expect(onboardingIdMatch).toBeTruthy();
|
||||||
|
const onboardingId = onboardingIdMatch?.[1];
|
||||||
|
|
||||||
|
// Step 3: Switch to owner user
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await ownerUser.apiLogin();
|
||||||
|
|
||||||
|
// Step 4: Owner opens resume URL and completes the onboarding wizard
|
||||||
|
await page.goto(`/settings/organizations/new/resume?onboardingId=${onboardingId}`);
|
||||||
|
|
||||||
|
const memberEmail = users.trackEmail({ username: "member1", domain: "example.com" });
|
||||||
|
const result = await completeOnboardingWizard(page, {
|
||||||
|
aboutText: "This is our test organization",
|
||||||
|
teams: ["Engineering", "Marketing"],
|
||||||
|
members: [memberEmail],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the organization was created successfully in the API response
|
||||||
|
expect(result.organizationId).toBeTruthy();
|
||||||
|
expect(result.name).toBe(orgName);
|
||||||
|
expect(result.slug).toBe(orgSlug);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Admin creates org for self - immediate creation", async ({ page, users }) => {
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
const orgName = "Admin Org";
|
||||||
|
const orgSlug = `admin-org-${uuid()}`.toLowerCase();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Fill form with admin's own email
|
||||||
|
await page.locator("input[name=orgOwnerEmail]").fill(admin.email);
|
||||||
|
await page.locator("input[name=name]").fill(orgName);
|
||||||
|
await page.locator("input[name=slug]").fill(orgSlug);
|
||||||
|
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
await page.locator("input[name=seats]").fill("5");
|
||||||
|
await page.locator("input[name=pricePerSeat]").fill("20");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit - admin creating for self goes through wizard
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Admin creating for self goes through the onboarding wizard
|
||||||
|
const result = await completeOnboardingWizard(page, {
|
||||||
|
aboutText: "Admin's organization",
|
||||||
|
teams: ["Operations"],
|
||||||
|
members: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the organization was created successfully
|
||||||
|
expect(result.organizationId).toBeTruthy();
|
||||||
|
expect(result.name).toBe(orgName);
|
||||||
|
expect(result.slug).toBe(orgSlug);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Admin handover URL structure is correct", async ({ page, users }) => {
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
const ownerEmail = users.trackEmail({
|
||||||
|
username: "testowner",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
await users.create({
|
||||||
|
username: "testowner",
|
||||||
|
email: ownerEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgName = "Handover Test";
|
||||||
|
const orgSlug = `handover-${uuid()}`.toLowerCase();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
await page.locator("input[name=orgOwnerEmail]").fill(ownerEmail);
|
||||||
|
await page.locator("input[name=name]").fill(orgName);
|
||||||
|
await page.locator("input[name=slug]").fill(orgSlug);
|
||||||
|
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
await page.locator("input[name=seats]").fill("10");
|
||||||
|
await page.locator("input[name=pricePerSeat]").fill("15");
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsePromise = page.waitForResponse("**/api/trpc/organizations/intentToCreateOrg**");
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Wait for the response
|
||||||
|
await responsePromise;
|
||||||
|
|
||||||
|
// Wait for redirect to handover page
|
||||||
|
await page.waitForURL("**/settings/organizations/new/handover");
|
||||||
|
|
||||||
|
// Wait for handover page elements to be visible (with longer timeout for store to populate)
|
||||||
|
await expect(page.getByTestId("onboarding-url")).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(page.getByTestId("copy-onboarding-url")).toBeVisible();
|
||||||
|
|
||||||
|
const onboardingUrl = await page.getByTestId("onboarding-url").textContent();
|
||||||
|
|
||||||
|
// Verify URL format
|
||||||
|
expect(onboardingUrl).toMatch(/\/settings\/organizations\/new\/resume\?onboardingId=[\w-]+/);
|
||||||
|
|
||||||
|
// Verify copy button is clickable (toast message may vary by locale)
|
||||||
|
await page.getByTestId("copy-onboarding-url").click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Non-Admin User Flow", () => {
|
||||||
|
test("Regular user creates org through full wizard", async ({ page, users }) => {
|
||||||
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
||||||
|
|
||||||
|
const user = await users.create({
|
||||||
|
username: "regularuser",
|
||||||
|
email: users.trackEmail({ username: "regularuser", domain: "example.com" }),
|
||||||
|
});
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
const orgName = "User Organization";
|
||||||
|
const orgSlug = `user-org-${uuid()}`.toLowerCase();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Verify user email is pre-filled and disabled
|
||||||
|
const emailInput = page.locator("input[name=orgOwnerEmail]");
|
||||||
|
await expect(emailInput).toBeDisabled();
|
||||||
|
await expect(emailInput).toHaveValue(user.email);
|
||||||
|
|
||||||
|
// Verify billing fields are NOT visible to regular users
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
await expect(page.locator("input[name=seats]")).not.toBeVisible();
|
||||||
|
await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Verify "Upgrade to Organizations" UI is shown
|
||||||
|
await expect(page.getByText(/upgrade to organizations/i)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill form
|
||||||
|
await page.locator("input[name=name]").fill(orgName);
|
||||||
|
await page.locator("input[name=slug]").fill(orgSlug);
|
||||||
|
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Verify navigated to /about step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/about");
|
||||||
|
|
||||||
|
// Complete about step
|
||||||
|
await page.locator('textarea[name="about"]').fill("User's organization description");
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Complete add-teams step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/add-teams");
|
||||||
|
await page.getByTestId("team.0.name").fill("Team Alpha");
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Complete onboard-members step
|
||||||
|
await page.waitForURL("**/settings/organizations/new/onboard-members");
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForResponse("**/api/trpc/organizations/intentToCreateOrg**"),
|
||||||
|
page.getByTestId("publish-button").click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// In billing enabled mode, should redirect to Stripe or payment
|
||||||
|
// In billing disabled mode, org created immediately
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
// Would redirect to Stripe checkout
|
||||||
|
await page.waitForURL(/checkout\.stripe\.com|\/settings\/organizations/);
|
||||||
|
} else {
|
||||||
|
// After org creation, session update causes redirect to profile page
|
||||||
|
// Wait for it to load, then navigate to organizations
|
||||||
|
await page.waitForURL("**/settings/my-account/profile");
|
||||||
|
await page.goto("/settings/organizations");
|
||||||
|
await page.waitForURL(/\/settings\/organizations/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Regular user - email field is disabled", async ({ page, users }) => {
|
||||||
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
||||||
|
|
||||||
|
const user = await users.create({
|
||||||
|
username: "testuser",
|
||||||
|
email: users.trackEmail({ username: "testuser", domain: "company.com" }),
|
||||||
|
});
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Verify email field state
|
||||||
|
const emailInput = page.locator("input[name=orgOwnerEmail]");
|
||||||
|
await expect(emailInput).toBeVisible();
|
||||||
|
await expect(emailInput).toBeDisabled();
|
||||||
|
await expect(emailInput).toHaveValue(user.email);
|
||||||
|
|
||||||
|
// Verify name and slug are derived from email domain
|
||||||
|
const nameInput = page.locator("input[name=name]");
|
||||||
|
const slugInput = page.locator("input[name=slug]");
|
||||||
|
|
||||||
|
// Name should be capitalized version of domain
|
||||||
|
const nameValue = await nameInput.inputValue();
|
||||||
|
expect(nameValue).toBeTruthy();
|
||||||
|
expect(nameValue).toMatch(/company/i);
|
||||||
|
|
||||||
|
// Slug should be domain-based
|
||||||
|
const slugValue = await slugInput.inputValue();
|
||||||
|
expect(slugValue).toBeTruthy();
|
||||||
|
expect(slugValue).toContain("company");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Form Validation", () => {
|
||||||
|
test("Admin form - required fields validation", async ({ page, users }) => {
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Submit empty form
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Should show validation errors
|
||||||
|
await expect(page.getByTestId("field-error")).toHaveCount(2); // name and slug required
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Slug already taken shows error", async ({ page, users, orgs }) => {
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
// Create an existing org with a specific slug
|
||||||
|
const existingSlug = `existing-${uuid()}`.toLowerCase();
|
||||||
|
await orgs.create({
|
||||||
|
name: "Existing Org",
|
||||||
|
slug: existingSlug,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
const ownerEmail = users.trackEmail({ username: "owner", domain: "example.com" });
|
||||||
|
await users.create({ username: "owner", email: ownerEmail });
|
||||||
|
|
||||||
|
await page.locator("input[name=orgOwnerEmail]").fill(ownerEmail);
|
||||||
|
await page.locator("input[name=name]").fill("New Org");
|
||||||
|
await page.locator("input[name=slug]").fill(existingSlug);
|
||||||
|
|
||||||
|
if (IS_TEAM_BILLING_ENABLED) {
|
||||||
|
await page.locator("input[name=seats]").fill("5");
|
||||||
|
await page.locator("input[name=pricePerSeat]").fill("10");
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Should show slug taken error
|
||||||
|
await expect(page.getByText(/url.*taken|already.*exists/i)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Navigation Between Steps", () => {
|
||||||
|
test("User can navigate through wizard steps", async ({ page, users }) => {
|
||||||
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
||||||
|
|
||||||
|
const user = await users.create({
|
||||||
|
username: "wizarduser",
|
||||||
|
email: users.trackEmail({ username: "wizarduser", domain: "example.com" }),
|
||||||
|
});
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
const orgName = "Wizard Test";
|
||||||
|
const orgSlug = `wizard-${uuid()}`.toLowerCase();
|
||||||
|
|
||||||
|
// Step 1
|
||||||
|
await page.locator("input[name=name]").fill(orgName);
|
||||||
|
await page.locator("input[name=slug]").fill(orgSlug);
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Step 2: About
|
||||||
|
await page.waitForURL("**/settings/organizations/new/about");
|
||||||
|
await page.locator('textarea[name="about"]').fill("Wizard test org");
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Step 3: Add teams
|
||||||
|
await page.waitForURL("**/settings/organizations/new/add-teams");
|
||||||
|
await page.getByTestId("team.0.name").fill("Team 1");
|
||||||
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
// Step 4: Onboard members
|
||||||
|
await page.waitForURL("**/settings/organizations/new/onboard-members");
|
||||||
|
|
||||||
|
// Verify wizard shows correct step numbers
|
||||||
|
// The WizardLayout component shows currentStep/maxSteps
|
||||||
|
// Just verify we reached the last step
|
||||||
|
await expect(page.getByTestId("publish-button")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Billing Field Visibility", () => {
|
||||||
|
test("Admin sees billing fields when billing enabled", async ({ page, users }) => {
|
||||||
|
test.skip(!IS_TEAM_BILLING_ENABLED, "Billing is not enabled");
|
||||||
|
|
||||||
|
const admin = await users.create({ role: "ADMIN" });
|
||||||
|
await admin.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Verify billing fields are visible for admin
|
||||||
|
await expect(page.locator("input[name=seats]")).toBeVisible();
|
||||||
|
await expect(page.locator("input[name=pricePerSeat]")).toBeVisible();
|
||||||
|
await expect(page.locator("#billingPeriod")).toBeVisible();
|
||||||
|
|
||||||
|
// Verify toggle between monthly/annual
|
||||||
|
await expect(page.getByText("Monthly")).toBeVisible();
|
||||||
|
await expect(page.getByText("Annually")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Non-admin sees pricing UI but not billing fields", async ({ page, users }) => {
|
||||||
|
test.skip(!IS_TEAM_BILLING_ENABLED, "Billing is not enabled");
|
||||||
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
||||||
|
|
||||||
|
const user = await users.create({
|
||||||
|
username: "pricinguser",
|
||||||
|
email: users.trackEmail({ username: "pricinguser", domain: "example.com" }),
|
||||||
|
});
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/settings/organizations/new");
|
||||||
|
|
||||||
|
// Should NOT see admin billing fields
|
||||||
|
await expect(page.locator("input[name=seats]")).not.toBeVisible();
|
||||||
|
await expect(page.locator("input[name=pricePerSeat]")).not.toBeVisible();
|
||||||
|
await expect(page.locator("#billingPeriod")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Should see upgrade/pricing UI
|
||||||
|
await expect(page.getByText(/upgrade to organizations/i)).toBeVisible();
|
||||||
|
await expect(page.getByText(/organization/i)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,475 +0,0 @@
|
|||||||
import type { Page } from "@playwright/test";
|
|
||||||
import { expect } from "@playwright/test";
|
|
||||||
import { JSDOM } from "jsdom";
|
|
||||||
import type { Messages } from "mailhog";
|
|
||||||
import path from "path";
|
|
||||||
import { uuid } from "short-uuid";
|
|
||||||
|
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
|
||||||
|
|
||||||
import type { createEmailsFixture } from "../fixtures/emails";
|
|
||||||
import { test } from "../lib/fixtures";
|
|
||||||
import { fillStripeTestCheckout } from "../lib/testUtils";
|
|
||||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
|
||||||
|
|
||||||
async function expectEmailWithSubject(
|
|
||||||
page: Page,
|
|
||||||
emails: ReturnType<typeof createEmailsFixture>,
|
|
||||||
userEmail: string,
|
|
||||||
subject: string
|
|
||||||
) {
|
|
||||||
if (!emails) return null;
|
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
|
||||||
|
|
||||||
const allEmails = (receivedEmails as Messages).items;
|
|
||||||
const email = allEmails.find((email) => email.subject === subject);
|
|
||||||
if (!email) {
|
|
||||||
throw new Error(`Email with subject ${subject} not found`);
|
|
||||||
}
|
|
||||||
const dom = new JSDOM(email.html);
|
|
||||||
return dom;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectOrganizationCreationEmailToBeSent({
|
|
||||||
page,
|
|
||||||
emails,
|
|
||||||
userEmail,
|
|
||||||
orgSlug,
|
|
||||||
}: {
|
|
||||||
page: Page;
|
|
||||||
emails: ReturnType<typeof createEmailsFixture>;
|
|
||||||
userEmail: string;
|
|
||||||
orgSlug: string;
|
|
||||||
}) {
|
|
||||||
const dom = await expectEmailWithSubject(page, emails, userEmail, "Your organization has been created");
|
|
||||||
const document = dom?.window?.document;
|
|
||||||
expect(document?.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
|
|
||||||
return dom;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectOrganizationCreationEmailToBeSentWithLinks({
|
|
||||||
page,
|
|
||||||
emails,
|
|
||||||
userEmail,
|
|
||||||
oldUsername,
|
|
||||||
newUsername,
|
|
||||||
orgSlug,
|
|
||||||
}: {
|
|
||||||
page: Page;
|
|
||||||
emails: ReturnType<typeof createEmailsFixture>;
|
|
||||||
userEmail: string;
|
|
||||||
oldUsername: string;
|
|
||||||
newUsername: string;
|
|
||||||
orgSlug: string;
|
|
||||||
}) {
|
|
||||||
const dom = await expectOrganizationCreationEmailToBeSent({
|
|
||||||
page,
|
|
||||||
emails,
|
|
||||||
userEmail,
|
|
||||||
orgSlug,
|
|
||||||
});
|
|
||||||
const document = dom?.window.document;
|
|
||||||
const links = document?.querySelectorAll(`[data-testid="organization-link-info"] [href]`);
|
|
||||||
if (!links) {
|
|
||||||
throw new Error(`data-testid="organization-link-info doesn't have links`);
|
|
||||||
}
|
|
||||||
expect((links[0] as unknown as HTMLAnchorElement).href).toContain(oldUsername);
|
|
||||||
expect((links[1] as unknown as HTMLAnchorElement).href).toContain(newUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectEmailVerificationEmailToBeSent(
|
|
||||||
page: Page,
|
|
||||||
emails: ReturnType<typeof createEmailsFixture>,
|
|
||||||
userEmail: string
|
|
||||||
) {
|
|
||||||
const subject = "Cal.com: Verify your account";
|
|
||||||
return expectEmailWithSubject(page, emails, userEmail, subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
test.afterEach(({ users, orgs }) => {
|
|
||||||
users.deleteAll();
|
|
||||||
orgs.deleteAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
function capitalize(text: string) {
|
|
||||||
if (!text) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe("Organization", () => {
|
|
||||||
test("Admin should be able to do onboarding handover an org where an existing user is made an owner", async ({
|
|
||||||
page,
|
|
||||||
users,
|
|
||||||
emails,
|
|
||||||
}) => {
|
|
||||||
const appLevelAdmin = await users.create({
|
|
||||||
role: "ADMIN",
|
|
||||||
});
|
|
||||||
await appLevelAdmin.apiLogin();
|
|
||||||
|
|
||||||
const orgOwnerUsernamePrefix = "owner";
|
|
||||||
|
|
||||||
const orgOwnerEmail = users.trackEmail({
|
|
||||||
username: orgOwnerUsernamePrefix,
|
|
||||||
domain: `example.com`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const orgOwnerUser = await users.create({
|
|
||||||
username: orgOwnerUsernamePrefix,
|
|
||||||
email: orgOwnerEmail,
|
|
||||||
role: "ADMIN",
|
|
||||||
});
|
|
||||||
|
|
||||||
const orgOwnerUsernameOutsideOrg = orgOwnerUser.username;
|
|
||||||
const orgOwnerUsernameInOrg = orgOwnerEmail.split("@")[0];
|
|
||||||
const orgName = capitalize(`${orgOwnerUser.username}`);
|
|
||||||
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
|
|
||||||
await page.goto("/settings/organizations/new");
|
|
||||||
|
|
||||||
await test.step("Basic info", async () => {
|
|
||||||
// Check required fields
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
await expect(page.getByTestId("field-error")).toHaveCount(2);
|
|
||||||
|
|
||||||
// Happy path
|
|
||||||
await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Handover", async () => {
|
|
||||||
const onboardingUrl = await page.getByTestId("onboarding-url").textContent();
|
|
||||||
expect(onboardingUrl).toContain("settings/organizations/new");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.skip("Admin should be able to create an org where the owner doesn't exist yet", async ({
|
|
||||||
page,
|
|
||||||
users,
|
|
||||||
emails,
|
|
||||||
}) => {
|
|
||||||
const appLevelAdmin = await users.create({
|
|
||||||
role: "ADMIN",
|
|
||||||
});
|
|
||||||
await appLevelAdmin.apiLogin();
|
|
||||||
const orgOwnerUsername = `owner`;
|
|
||||||
const orgName = capitalize(`${orgOwnerUsername}`);
|
|
||||||
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
|
|
||||||
const orgOwnerEmail = users.trackEmail({
|
|
||||||
username: orgOwnerUsername,
|
|
||||||
domain: `example.com`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto("/settings/organizations/new");
|
|
||||||
|
|
||||||
await test.step("Basic info", async () => {
|
|
||||||
// Check required fields
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
await expect(page.getByTestId("field-error")).toHaveCount(2);
|
|
||||||
|
|
||||||
// Happy path
|
|
||||||
await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
|
|
||||||
});
|
|
||||||
|
|
||||||
const dom = await expectOrganizationCreationEmailToBeSent({
|
|
||||||
page,
|
|
||||||
emails,
|
|
||||||
userEmail: orgOwnerEmail,
|
|
||||||
orgSlug,
|
|
||||||
});
|
|
||||||
expect(dom?.window.document.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
|
|
||||||
await expectEmailVerificationEmailToBeSent(page, emails, orgOwnerEmail);
|
|
||||||
// Rest of the steps remain same as org creation with existing user as owner. So skipping them
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User can create and upgrade a org", async ({ page, users, emails }) => {
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
|
||||||
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
|
||||||
const stringUUID = uuid();
|
|
||||||
|
|
||||||
const orgOwnerUsername = `owner-${stringUUID}`;
|
|
||||||
|
|
||||||
const targetOrgEmail = users.trackEmail({
|
|
||||||
username: orgOwnerUsername,
|
|
||||||
domain: `example.com`,
|
|
||||||
});
|
|
||||||
const orgOwnerUser = await users.create({
|
|
||||||
username: orgOwnerUsername,
|
|
||||||
email: targetOrgEmail,
|
|
||||||
});
|
|
||||||
|
|
||||||
await orgOwnerUser.apiLogin();
|
|
||||||
const orgName = capitalize(`${orgOwnerUsername}`);
|
|
||||||
await page.goto("/settings/organizations/new");
|
|
||||||
|
|
||||||
await test.step("Basic info", async () => {
|
|
||||||
// These values are inferred due to an existing user being signed
|
|
||||||
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
|
|
||||||
expect(await page.locator("input[name=slug]").inputValue()).toBe("example");
|
|
||||||
|
|
||||||
await page.locator("input[name=name]").fill(orgName);
|
|
||||||
await page.locator("input[name=slug]").fill(orgOwnerUsername);
|
|
||||||
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("About the organization", async () => {
|
|
||||||
// Choosing an avatar
|
|
||||||
await page.getByTestId("open-upload-avatar-dialog").click();
|
|
||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
||||||
await page.getByText("Choose a file...").click();
|
|
||||||
const fileChooser = await fileChooserPromise;
|
|
||||||
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
|
||||||
await page.getByTestId("upload-avatar").click();
|
|
||||||
|
|
||||||
// About text
|
|
||||||
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
|
|
||||||
// Waiting to be in next step URL
|
|
||||||
await page.waitForURL("/settings/organizations/*/onboard-members");
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("On-board administrators", async () => {
|
|
||||||
await page.waitForSelector('[data-testid="pending-member-list"]');
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
||||||
|
|
||||||
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
|
|
||||||
|
|
||||||
//can add members
|
|
||||||
await page.getByTestId("new-member-button").click();
|
|
||||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
|
|
||||||
await page.getByTestId("invite-new-member-button").click();
|
|
||||||
await expect(page.getByTestId("pending-member-item").filter({ hasText: adminEmail })).toBeVisible();
|
|
||||||
// TODO: Check if invited admin received the invitation email
|
|
||||||
// await expectInvitationEmailToBeReceived(
|
|
||||||
// page,
|
|
||||||
// emails,
|
|
||||||
// adminEmail,
|
|
||||||
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
|
||||||
// );
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
||||||
|
|
||||||
// can remove members
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
||||||
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
|
||||||
await lastRemoveMemberButton.click();
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
||||||
await page.getByTestId("publish-button").click();
|
|
||||||
// Waiting to be in next step URL
|
|
||||||
await page.waitForURL("/settings/organizations/*/add-teams");
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Create teams", async () => {
|
|
||||||
// Filling one team
|
|
||||||
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
|
||||||
|
|
||||||
// Adding another team
|
|
||||||
await page.getByTestId("add_a_team").click();
|
|
||||||
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
|
||||||
|
|
||||||
// Finishing the creation wizard
|
|
||||||
await page.getByTestId("continue_or_checkout").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Login as org owner and pay", async () => {
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
|
||||||
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
|
|
||||||
await orgOwnerUser.apiLogin();
|
|
||||||
await page.goto("/event-types");
|
|
||||||
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
|
|
||||||
|
|
||||||
await expect(upgradeButton).toBeVisible();
|
|
||||||
await upgradeButton.click();
|
|
||||||
// Check that stripe checkout is present
|
|
||||||
const expectedUrl = "https://checkout.stripe.com";
|
|
||||||
|
|
||||||
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
|
|
||||||
const url = page.url();
|
|
||||||
|
|
||||||
// Check that the URL matches the expected URL
|
|
||||||
expect(url).toContain(expectedUrl);
|
|
||||||
|
|
||||||
await fillStripeTestCheckout(page);
|
|
||||||
|
|
||||||
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
|
|
||||||
|
|
||||||
await expect(upgradeButtonHidden).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User gets prompted with >=3 teams to upgrade & can transfer existing teams to org", async ({
|
|
||||||
page,
|
|
||||||
users,
|
|
||||||
}) => {
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
|
||||||
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
|
||||||
const numberOfTeams = 3;
|
|
||||||
const stringUUID = uuid();
|
|
||||||
|
|
||||||
const orgOwnerUsername = `owner-${stringUUID}`;
|
|
||||||
|
|
||||||
const targetOrgEmail = users.trackEmail({
|
|
||||||
username: orgOwnerUsername,
|
|
||||||
domain: `example.com`,
|
|
||||||
});
|
|
||||||
const orgOwnerUser = await users.create(
|
|
||||||
{
|
|
||||||
username: orgOwnerUsername,
|
|
||||||
email: targetOrgEmail,
|
|
||||||
},
|
|
||||||
{ hasTeam: true, numberOfTeams }
|
|
||||||
);
|
|
||||||
|
|
||||||
await orgOwnerUser.apiLogin();
|
|
||||||
|
|
||||||
await page.goto("/teams");
|
|
||||||
|
|
||||||
await test.step("Has org self serve banner", async () => {
|
|
||||||
// These values are inferred due to an existing user being signed
|
|
||||||
const selfServeButtonLocator = await page.getByTestId("setup_your_org_action_button");
|
|
||||||
await expect(selfServeButtonLocator).toBeVisible();
|
|
||||||
|
|
||||||
await selfServeButtonLocator.click();
|
|
||||||
await page.waitForURL("/settings/organizations/new");
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Basic info", async () => {
|
|
||||||
// These values are inferred due to an existing user being signed
|
|
||||||
const slugLocator = await page.locator("input[name=slug]");
|
|
||||||
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
|
|
||||||
expect(await slugLocator.inputValue()).toBe("example");
|
|
||||||
|
|
||||||
await slugLocator.fill(`example-${stringUUID}`);
|
|
||||||
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("About the organization", async () => {
|
|
||||||
// Choosing an avatar
|
|
||||||
await page.getByTestId("open-upload-avatar-dialog").click();
|
|
||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
||||||
await page.getByText("Choose a file...").click();
|
|
||||||
const fileChooser = await fileChooserPromise;
|
|
||||||
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
|
||||||
await page.getByTestId("upload-avatar").click();
|
|
||||||
|
|
||||||
// About text
|
|
||||||
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
|
||||||
await page.locator("button[type=submit]").click();
|
|
||||||
|
|
||||||
// Waiting to be in next step URL
|
|
||||||
await page.waitForURL("/settings/organizations/*/onboard-members");
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("On-board administrators", async () => {
|
|
||||||
await page.waitForSelector('[data-testid="pending-member-list"]');
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
||||||
|
|
||||||
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
|
|
||||||
|
|
||||||
//can add members
|
|
||||||
await page.getByTestId("new-member-button").click();
|
|
||||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
|
|
||||||
await page.getByTestId("invite-new-member-button").click();
|
|
||||||
await expect(page.getByTestId("pending-member-item").filter({ hasText: adminEmail })).toBeVisible();
|
|
||||||
// TODO: Check if invited admin received the invitation email
|
|
||||||
// await expectInvitationEmailToBeReceived(
|
|
||||||
// page,
|
|
||||||
// emails,
|
|
||||||
// adminEmail,
|
|
||||||
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
|
||||||
// );
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
||||||
|
|
||||||
// can remove members
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
||||||
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
|
||||||
await lastRemoveMemberButton.click();
|
|
||||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
||||||
await page.getByTestId("publish-button").click();
|
|
||||||
// Waiting to be in next step URL
|
|
||||||
await page.waitForURL("/settings/organizations/*/add-teams");
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Move existing teams to org", async () => {
|
|
||||||
// No easy way to get all team checkboxes so we fill all checkboxes on the page in
|
|
||||||
const foundCheckboxes = page.locator('input[type="checkbox"]');
|
|
||||||
const count = await foundCheckboxes.count();
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const checkbox = foundCheckboxes.nth(i);
|
|
||||||
await checkbox.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Create teams", async () => {
|
|
||||||
// Filling one team
|
|
||||||
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
|
||||||
|
|
||||||
// Adding another team
|
|
||||||
await page.getByTestId("new-team-dialog-button").click();
|
|
||||||
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
|
||||||
|
|
||||||
// Finishing the creation wizard
|
|
||||||
await page.getByTestId("continue_or_checkout").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Login as org owner and pay", async () => {
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
|
||||||
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
|
|
||||||
await orgOwnerUser.apiLogin();
|
|
||||||
await page.goto("/event-types");
|
|
||||||
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
|
|
||||||
|
|
||||||
await expect(upgradeButton).toBeVisible();
|
|
||||||
await upgradeButton.click();
|
|
||||||
// Check that stripe checkout is present
|
|
||||||
const expectedUrl = "https://checkout.stripe.com";
|
|
||||||
|
|
||||||
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
|
|
||||||
const url = page.url();
|
|
||||||
|
|
||||||
// Check that the URL matches the expected URL
|
|
||||||
expect(url).toContain(expectedUrl);
|
|
||||||
|
|
||||||
await fillStripeTestCheckout(page);
|
|
||||||
|
|
||||||
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
|
|
||||||
|
|
||||||
await expect(upgradeButtonHidden).toBeHidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Ensure correctnumberOfTeams are migrated", async () => {
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
|
||||||
await page.goto("/teams");
|
|
||||||
const teamListItems = await page.getByTestId("team-list-item-link").all();
|
|
||||||
|
|
||||||
// Number of teams migrated + the two created in the create teams step
|
|
||||||
expect(teamListItems.length).toBe(numberOfTeams + 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fillAndSubmitFirstStepAsAdmin(
|
|
||||||
page: Page,
|
|
||||||
targetOrgEmail: string,
|
|
||||||
orgName: string,
|
|
||||||
orgSlug: string
|
|
||||||
) {
|
|
||||||
await page.locator("input[name=orgOwnerEmail]").fill(targetOrgEmail);
|
|
||||||
// Since we are admin fill in this information instead of deriving it
|
|
||||||
await page.locator("input[name=name]").fill(orgName);
|
|
||||||
await page.locator("input[name=slug]").fill(orgSlug);
|
|
||||||
|
|
||||||
// Fill in seat information
|
|
||||||
await page.locator("input[name=seats]").fill("30");
|
|
||||||
await page.locator("input[name=pricePerSeat]").fill("30");
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForResponse("**/api/trpc/organizations/intentToCreateOrg**"),
|
|
||||||
page.locator("button[type=submit]").click(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -653,11 +653,12 @@ test.describe("Out of office", () => {
|
|||||||
test("Default date range filter set to `Last 7 Days`", async ({ page, users }) => {
|
test("Default date range filter set to `Last 7 Days`", async ({ page, users }) => {
|
||||||
const user = await users.create({ name: `userOne=${Date.now()}` });
|
const user = await users.create({ name: `userOne=${Date.now()}` });
|
||||||
await user.apiLogin();
|
await user.apiLogin();
|
||||||
await page.goto("/settings/my-account/out-of-office");
|
const responsePromise = page.waitForResponse(
|
||||||
await page.waitForLoadState("domcontentloaded");
|
|
||||||
await page.waitForResponse(
|
|
||||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||||
);
|
);
|
||||||
|
await page.goto("/settings/my-account/out-of-office");
|
||||||
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
await responsePromise;
|
||||||
await addFilter(page, "dateRange");
|
await addFilter(page, "dateRange");
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('[data-testid="filter-popover-trigger-dateRange"]', { hasText: "Last 7 Days" }).first()
|
page.locator('[data-testid="filter-popover-trigger-dateRange"]', { hasText: "Last 7 Days" }).first()
|
||||||
@@ -667,11 +668,12 @@ test.describe("Out of office", () => {
|
|||||||
test("Can choose date range presets", async ({ page, users }) => {
|
test("Can choose date range presets", async ({ page, users }) => {
|
||||||
const user = await users.create({ name: `userOne=${Date.now()}` });
|
const user = await users.create({ name: `userOne=${Date.now()}` });
|
||||||
await user.apiLogin();
|
await user.apiLogin();
|
||||||
await page.goto("/settings/my-account/out-of-office");
|
const responsePromise = page.waitForResponse(
|
||||||
await page.waitForLoadState("domcontentloaded");
|
|
||||||
await page.waitForResponse(
|
|
||||||
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
(response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
|
||||||
);
|
);
|
||||||
|
await page.goto("/settings/my-account/out-of-office");
|
||||||
|
await page.waitForLoadState("domcontentloaded");
|
||||||
|
await responsePromise;
|
||||||
await addFilter(page, "dateRange");
|
await addFilter(page, "dateRange");
|
||||||
|
|
||||||
await expect(page.locator('[data-testid="date-range-options-tdy"]')).toBeVisible(); //Today
|
await expect(page.locator('[data-testid="date-range-options-tdy"]')).toBeVisible(); //Today
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import { selectFirstAvailableTimeSlotNextMonth, submitAndWaitForResponse } from "./lib/testUtils";
|
import { selectFirstAvailableTimeSlotNextMonth, submitAndWaitForResponse } from "./lib/testUtils";
|
||||||
@@ -8,6 +9,11 @@ import { selectFirstAvailableTimeSlotNextMonth, submitAndWaitForResponse } from
|
|||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
|
async function goToAppsTab(page: Page, eventTypeId?: number) {
|
||||||
|
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
|
||||||
|
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page");
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("Payment app", () => {
|
test.describe("Payment app", () => {
|
||||||
test("Should be able to edit alby price, currency", async ({ page, users }) => {
|
test("Should be able to edit alby price, currency", async ({ page, users }) => {
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
@@ -29,7 +35,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
await page.getByPlaceholder("Price").click();
|
await page.getByPlaceholder("Price").click();
|
||||||
@@ -75,7 +81,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
await page.getByTestId("stripe-currency-select").click();
|
await page.getByTestId("stripe-currency-select").click();
|
||||||
await page.getByTestId("select-option-usd").click();
|
await page.getByTestId("select-option-usd").click();
|
||||||
@@ -115,7 +121,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
|
|
||||||
@@ -158,7 +164,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
|
|
||||||
@@ -220,7 +226,7 @@ test.describe("Payment app", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent?.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
// make sure Tracking ID is displayed
|
// make sure Tracking ID is displayed
|
||||||
@@ -267,7 +273,7 @@ test.describe("Payment app", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.locator("[data-testid='paypal-app-switch']").click();
|
await page.locator("[data-testid='paypal-app-switch']").click();
|
||||||
await page.locator("[data-testid='stripe-app-switch']").isDisabled();
|
await page.locator("[data-testid='stripe-app-switch']").isDisabled();
|
||||||
@@ -314,7 +320,7 @@ test.describe("Payment app", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
await goToAppsTab(page, paymentEvent?.id);
|
||||||
|
|
||||||
await page.getByTestId("paypal-app-switch").click();
|
await page.getByTestId("paypal-app-switch").click();
|
||||||
await page.getByTestId("paypal-price-input").fill("100");
|
await page.getByTestId("paypal-price-input").fill("100");
|
||||||
|
|||||||
@@ -3299,6 +3299,12 @@
|
|||||||
"organization_onboarding_already_completed": "Organization onboarding already completed",
|
"organization_onboarding_already_completed": "Organization onboarding already completed",
|
||||||
"organization_onboarding_already_exists": "Organization onboarding already exists",
|
"organization_onboarding_already_exists": "Organization onboarding already exists",
|
||||||
"organization_onboarding_not_found": "Organization onboarding not found",
|
"organization_onboarding_not_found": "Organization onboarding not found",
|
||||||
|
"no_onboarding_id_provided": "No onboarding ID provided",
|
||||||
|
"onboarding_not_found": "Onboarding not found",
|
||||||
|
"onboarding_already_complete": "Onboarding already complete",
|
||||||
|
"onboarding_already_complete_description": "This onboarding has already been completed. You can view your organization in settings.",
|
||||||
|
"resume_onboarding": "Resume Onboarding",
|
||||||
|
"resume_onboarding_description": "Continue with your organization onboarding process",
|
||||||
"payment_successful": "Payment successful!",
|
"payment_successful": "Payment successful!",
|
||||||
"handover_onboarding_page_title": "Handover onboarding",
|
"handover_onboarding_page_title": "Handover onboarding",
|
||||||
"handover_onboarding_page_description": "Handover onboarding to the user",
|
"handover_onboarding_page_description": "Handover onboarding to the user",
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import { encode } from "querystring";
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic";
|
|
||||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
|
||||||
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
|
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||||
|
import { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic";
|
||||||
|
import { getBrandingForUser } from "@calcom/features/profile/lib/getBranding";
|
||||||
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
|
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
|
||||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { EventType, User } from "@calcom/prisma/client";
|
import type { EventType, User } from "@calcom/prisma/client";
|
||||||
@@ -33,6 +34,9 @@ type UserPageProps = {
|
|||||||
requestedSlug: string | null;
|
requestedSlug: string | null;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
id: number | null;
|
id: number | null;
|
||||||
|
brandColor: string | null;
|
||||||
|
darkBrandColor: string | null;
|
||||||
|
theme: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
allowSEOIndexing: boolean;
|
allowSEOIndexing: boolean;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
@@ -130,15 +134,18 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
|||||||
|
|
||||||
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
|
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
|
||||||
|
|
||||||
|
const branding = getBrandingForUser({ user });
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
name: user.name || user.username || "",
|
name: user.name || user.username || "",
|
||||||
image: getUserAvatarUrl({
|
image: getUserAvatarUrl({
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
}),
|
}),
|
||||||
theme: user.theme,
|
theme: branding.theme,
|
||||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
brandColor: branding.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
darkBrandColor:
|
||||||
|
branding.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
organization: user.profile.organization,
|
organization: user.profile.organization,
|
||||||
|
|||||||
@@ -17241,6 +17241,20 @@
|
|||||||
},
|
},
|
||||||
"required": ["type", "slug", "label", "required", "placeholder", "hidden", "isDefault"]
|
"required": ["type", "slug", "label", "required", "placeholder", "hidden", "isDefault"]
|
||||||
},
|
},
|
||||||
|
"BookerActiveBookingsLimitOutput_2024_06_14": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"maximumActiveBookings": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The maximum number of active bookings a booker can have for this event type.",
|
||||||
|
"example": 3
|
||||||
|
},
|
||||||
|
"offerReschedule": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to offer rescheduling the last active booking to the chosen time slot when limit is reached."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"EventTypeOutput_2024_06_14": {
|
"EventTypeOutput_2024_06_14": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -17428,7 +17442,7 @@
|
|||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"bookerActiveBookingsLimit": {
|
"bookerActiveBookingsLimit": {
|
||||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
"$ref": "#/components/schemas/BookerActiveBookingsLimitOutput_2024_06_14"
|
||||||
},
|
},
|
||||||
"onlyShowFirstAvailableSlot": {
|
"onlyShowFirstAvailableSlot": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -19729,7 +19743,7 @@
|
|||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"bookerActiveBookingsLimit": {
|
"bookerActiveBookingsLimit": {
|
||||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
"$ref": "#/components/schemas/BookerActiveBookingsLimitOutput_2024_06_14"
|
||||||
},
|
},
|
||||||
"onlyShowFirstAvailableSlot": {
|
"onlyShowFirstAvailableSlot": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
+8
-7
@@ -209,18 +209,19 @@
|
|||||||
"group": "Atoms",
|
"group": "Atoms",
|
||||||
"icon": "atom",
|
"icon": "atom",
|
||||||
"pages": [
|
"pages": [
|
||||||
"platform/atoms/cal-provider",
|
|
||||||
"platform/atoms/google-calendar-connect",
|
|
||||||
"platform/atoms/outlook-calendar-connect",
|
|
||||||
"platform/atoms/apple-calendar-connect",
|
"platform/atoms/apple-calendar-connect",
|
||||||
"platform/atoms/availability-settings",
|
"platform/atoms/availability-settings",
|
||||||
"platform/atoms/booker",
|
"platform/atoms/booker",
|
||||||
"platform/atoms/event-type",
|
"platform/atoms/cal-provider",
|
||||||
"platform/atoms/calendar-settings",
|
"platform/atoms/calendar-settings",
|
||||||
"platform/atoms/payment-form",
|
|
||||||
"platform/atoms/conferencing-apps",
|
|
||||||
"platform/atoms/calendar-view",
|
"platform/atoms/calendar-view",
|
||||||
"platform/atoms/create-schedule"
|
"platform/atoms/conferencing-apps",
|
||||||
|
"platform/atoms/create-schedule",
|
||||||
|
"platform/atoms/event-type",
|
||||||
|
"platform/atoms/google-calendar-connect",
|
||||||
|
"platform/atoms/list-schedules",
|
||||||
|
"platform/atoms/outlook-calendar-connect",
|
||||||
|
"platform/atoms/payment-form"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
title: "List schedules"
|
||||||
|
---
|
||||||
|
|
||||||
|
The List schedules atom displays all of a user's availability schedules and provides management actions such as delete, duplicate, and edit capabilities for each schedule.
|
||||||
|
|
||||||
|
Below code snippet can be used to render the List schedules atom
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { ListSchedules } from "@calcom/atoms";
|
||||||
|
|
||||||
|
export default function ListSchedules() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a demonstration of the List schedules atom, please refer to the video below.
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
height="315"
|
||||||
|
style={{ width: "100%", maxWidth: "560px" }}
|
||||||
|
src="https://www.loom.com/embed/8e020a4848d543548c519d3a7cd532c6?sid=e6ccad23-8372-402a-8fd3-c0180bc9113c"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen="true"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
Below is a list of props that can be passed to the List schedules atom.
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
| Name | Required | Description |
|
||||||
|
| :------------- | :------- | :---------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| getRedirectUrl | No | Callback function that determines where the user should be redirected when they click on a schedule from the atom |
|
||||||
|
|
||||||
|
## Combining list schedules atom with availability settings atom
|
||||||
|
|
||||||
|
The List schedules atom works best when combined with the Availability settings atom to provide a seamless experience. In order to combine both of atom, here are the steps we recommend:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Setup list schedules atom">
|
||||||
|
Create a page for demostrating the list schedules atom.
|
||||||
|
|
||||||
|
Below code snippet can be used to setup the list schedules atom on a new page
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { ListSchedules } from "@calcom/atoms";
|
||||||
|
|
||||||
|
export default function ListSchedules() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`getRedirectUrl` prop is a callback function that determines where the user should be redirected when they click on a schedule from the atom. Say if you want to display your availability settings atom on the page `/availability/:scheduleId`, you need to make sure you return the exact same url when you call the `getRedirectUrl` function.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Setup availability settings atom">
|
||||||
|
Create a page for demonstrating the availability settings atom. This should be a dynamic route that accepts a schedule id as a query parameter, and then the schedule id gets passed to the availability settings atom.
|
||||||
|
|
||||||
|
Below code snippet can be used to obtain the `scheduleId` and display the availability settings atom
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { AvailabilitySettings } from "@calcom/atoms";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
export default function Availability() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AvailabilitySettings
|
||||||
|
id={router.query.scheduleId as string}
|
||||||
|
onUpdateSuccess={() => {
|
||||||
|
console.log("Updated schedule successfully");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
-Below video shows a demonstration of how we combined the list schedules atom and availability settings atom in one of our example app to create a seamless availability management experience.
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
height="315"
|
||||||
|
style={{ width: "100%", maxWidth: "560px" }}
|
||||||
|
src="https://www.loom.com/embed/76b65330e61e47eb95b7072ed3a42ec8?sid=9373ec6f-ba55-431e-98de-5dd3fb252a8a"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen="true"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Link to the example app can be found
|
||||||
|
[here](https://github.com/calcom/cal.com/tree/main/packages/platform/examples/base)
|
||||||
|
</Info>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isInMemoryDelegationCredential } from "@calcom/lib/delegationCredential";
|
import { isInMemoryDelegationCredential } from "@calcom/lib/delegationCredential";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||||
|
|
||||||
import { getTokenObjectFromCredential } from "./getTokenObjectFromCredential";
|
import { getTokenObjectFromCredential } from "./getTokenObjectFromCredential";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
|
|
||||||
import { btcpayCredentialKeysSchema } from "../../lib/btcpayCredentialKeysSchema";
|
import { btcpayCredentialKeysSchema } from "../../lib/btcpayCredentialKeysSchema";
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|||||||
|
|
||||||
import { metadata as googleCalendarMetadata } from "@calcom/app-store/googlecalendar/_metadata";
|
import { metadata as googleCalendarMetadata } from "@calcom/app-store/googlecalendar/_metadata";
|
||||||
import { metadata as googleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
import { metadata as googleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
||||||
import type { ServiceAccountKey } from "@calcom/lib/server/repository/delegationCredential";
|
import type { ServiceAccountKey } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
|
||||||
import { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
|
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
|
||||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||||
import { SMSLockState, RRTimestampBasis } from "@calcom/prisma/enums";
|
import { SMSLockState, RRTimestampBasis } from "@calcom/prisma/enums";
|
||||||
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
|
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
|
||||||
@@ -28,7 +28,7 @@ vi.mock("@calcom/features/ee/organizations/repositories/OrganizationRepository",
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock DelegationCredentialRepository
|
// Mock DelegationCredentialRepository
|
||||||
vi.mock("@calcom/lib/server/repository/delegationCredential", () => ({
|
vi.mock("@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository", () => ({
|
||||||
DelegationCredentialRepository: {
|
DelegationCredentialRepository: {
|
||||||
findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey: vi.fn(),
|
findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey: vi.fn(),
|
||||||
findUniqueByOrganizationIdAndDomainIncludeSensitiveServiceAccountKey: vi.fn(),
|
findUniqueByOrganizationIdAndDomainIncludeSensitiveServiceAccountKey: vi.fn(),
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
} from "@calcom/lib/delegationCredential";
|
} from "@calcom/lib/delegationCredential";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import type { ServiceAccountKey } from "@calcom/lib/server/repository/delegationCredential";
|
import type { ServiceAccountKey } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
|
||||||
import { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
|
import { DelegationCredentialRepository } from "@calcom/features/delegation-credentials/repositories/DelegationCredentialRepository";
|
||||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dub } from "dub-package";
|
import { Dub } from "dub-package";
|
||||||
|
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService";
|
import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService";
|
||||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type {
|
|||||||
SimplePublicObjectInput,
|
SimplePublicObjectInput,
|
||||||
} from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
|
} from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
|
||||||
|
|
||||||
import getLabelValueMapFromResponses from "@calcom/features/bookings/lib/getLabelValueMapFromResponses";
|
|
||||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||||
|
import getLabelValueMapFromResponses from "@calcom/lib/bookings/getLabelValueMapFromResponses";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
@@ -21,10 +21,6 @@ import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
|||||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||||
import type { HubspotToken } from "../api/callback";
|
import type { HubspotToken } from "../api/callback";
|
||||||
|
|
||||||
interface CustomPublicObjectInput extends SimplePublicObjectInput {
|
|
||||||
id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class HubspotCalendarService implements CRM {
|
export default class HubspotCalendarService implements CRM {
|
||||||
private url = "";
|
private url = "";
|
||||||
private integrationName = "";
|
private integrationName = "";
|
||||||
@@ -117,8 +113,14 @@ export default class HubspotCalendarService implements CRM {
|
|||||||
return this.hubspotClient.crm.objects.meetings.basicApi.update(uid, simplePublicObjectInput);
|
return this.hubspotClient.crm.objects.meetings.basicApi.update(uid, simplePublicObjectInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
private hubspotDeleteMeeting = async (uid: string) => {
|
private hubspotCancelMeeting = async (uid: string) => {
|
||||||
return this.hubspotClient.crm.objects.meetings.basicApi.archive(uid);
|
const simplePublicObjectInput: SimplePublicObjectInput = {
|
||||||
|
properties: {
|
||||||
|
hs_meeting_outcome: "CANCELED",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.hubspotClient.crm.objects.meetings.basicApi.update(uid, simplePublicObjectInput);
|
||||||
};
|
};
|
||||||
|
|
||||||
private hubspotAuth = async (credential: CredentialPayload) => {
|
private hubspotAuth = async (credential: CredentialPayload) => {
|
||||||
@@ -204,7 +206,6 @@ export default class HubspotCalendarService implements CRM {
|
|||||||
return await this.handleMeetingCreation(event, contacts);
|
return await this.handleMeetingCreation(event, contacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||||
const auth = await this.auth;
|
const auth = await this.auth;
|
||||||
await auth.getToken();
|
await auth.getToken();
|
||||||
@@ -214,7 +215,7 @@ export default class HubspotCalendarService implements CRM {
|
|||||||
async deleteEvent(uid: string): Promise<void> {
|
async deleteEvent(uid: string): Promise<void> {
|
||||||
const auth = await this.auth;
|
const auth = await this.auth;
|
||||||
await auth.getToken();
|
await auth.getToken();
|
||||||
return await this.hubspotDeleteMeeting(uid);
|
await this.hubspotCancelMeeting(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
|
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
entityPrismaWhereClause,
|
||||||
|
canEditEntity,
|
||||||
|
} from "@calcom/features/pbac/lib/entityPermissionUtils.server";
|
||||||
import { sanitizeValue } from "@calcom/lib/csvUtils";
|
import { sanitizeValue } from "@calcom/lib/csvUtils";
|
||||||
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils.server";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
|||||||
|
|
||||||
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
|
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
|
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
|
||||||
import {
|
import {
|
||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
WorkflowTriggerEvents,
|
WorkflowTriggerEvents,
|
||||||
@@ -37,7 +37,7 @@ vi.mock("@calcom/features/tasker", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock workflow dependencies
|
// Mock workflow dependencies
|
||||||
vi.mock("@calcom/lib/server/service/workflows", () => ({
|
vi.mock("@calcom/features/ee/workflows/lib/service/WorkflowService", () => ({
|
||||||
WorkflowService: {
|
WorkflowService: {
|
||||||
getAllWorkflowsFromRoutingForm: vi.fn(() => Promise.resolve([])),
|
getAllWorkflowsFromRoutingForm: vi.fn(() => Promise.resolve([])),
|
||||||
scheduleFormWorkflows: vi.fn(() => Promise.resolve()),
|
scheduleFormWorkflows: vi.fn(() => Promise.resolve()),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import type { App_RoutingForms_Form, User } from "@calcom/prisma/client";
|
import type { App_RoutingForms_Form, User } from "@calcom/prisma/client";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils.server";
|
import { entityPrismaWhereClause } from "@calcom/features/pbac/lib/entityPermissionUtils.server";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { canCreateEntity, canEditEntity } from "@calcom/lib/entityPermissionUtils.server";
|
import { canCreateEntity, canEditEntity } from "@calcom/features/pbac/lib/entityPermissionUtils.server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { App_RoutingForms_Form, User } from "@calcom/prisma/client";
|
import type { App_RoutingForms_Form, User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
import { routingFormIncompleteBookingDataSchema } from "../../zod";
|
import { routingFormIncompleteBookingDataSchema } from "../../zod";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
|
|
||||||
import type { AttributeRoutingConfig } from "../../routing-forms/types/types";
|
import type { AttributeRoutingConfig } from "../../routing-forms/types/types";
|
||||||
import SalesforceCRMService from "./CrmService";
|
import SalesforceCRMService from "./CrmService";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
|
|
||||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +18,7 @@ import { validateAuthentication, buildReturnUrl } from "../portal";
|
|||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock("@calcom/features/pbac/services/permission-check.service");
|
vi.mock("@calcom/features/pbac/services/permission-check.service");
|
||||||
vi.mock("@calcom/lib/server/repository/team");
|
vi.mock("@calcom/features/ee/teams/repositories/TeamRepository");
|
||||||
vi.mock("../../lib/customer");
|
vi.mock("../../lib/customer");
|
||||||
vi.mock("../../lib/server");
|
vi.mock("../../lib/server");
|
||||||
vi.mock("../../lib/subscriptions");
|
vi.mock("../../lib/subscriptions");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PermissionCheckService } from "@calcom/features/pbac/services/permissio
|
|||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import stripe from "../../server";
|
import stripe from "../../server";
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import type { BillingPortalService } from "../base/BillingPortalService";
|
import type { BillingPortalService } from "../base/BillingPortalService";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import getLabelValueMapFromResponses from "@calcom/features/bookings/lib/getLabelValueMapFromResponses";
|
import getLabelValueMapFromResponses from "@calcom/lib/bookings/getLabelValueMapFromResponses";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { Info } from "./Info";
|
import { Info } from "./Info";
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import logger from "@calcom/lib/logger";
|
|||||||
import { randomString } from "@calcom/lib/random";
|
import { randomString } from "@calcom/lib/random";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { hashEmail } from "@calcom/lib/server/PiiHasher";
|
import { hashEmail } from "@calcom/lib/server/PiiHasher";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
||||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||||
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
|
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromReq
|
|||||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||||
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
||||||
|
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
|
||||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||||
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
||||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||||
@@ -15,7 +16,6 @@ import { HttpError } from "@calcom/lib/http-error";
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import type { CustomNextApiHandler } from "@calcom/lib/server/username";
|
import type { CustomNextApiHandler } from "@calcom/lib/server/username";
|
||||||
import { usernameHandler } from "@calcom/lib/server/username";
|
import { usernameHandler } from "@calcom/lib/server/username";
|
||||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
|
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { CreationSource } from "@calcom/prisma/enums";
|
import { CreationSource } from "@calcom/prisma/enums";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { NextResponse } from "next/server";
|
|||||||
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
|
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
|
||||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||||
|
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
|
||||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||||
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { isUsernameReservedDueToMigration } from "@calcom/lib/server/username";
|
import { isUsernameReservedDueToMigration } from "@calcom/lib/server/username";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { validateAndGetCorrectedUsernameInTeam } from "@calcom/features/auth/signup/utils/validateUsername";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { validateAndGetCorrectedUsernameInTeam } from "@calcom/lib/validateUsername";
|
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export async function findTokenByToken({ token }: { token: string }) {
|
export async function findTokenByToken({ token }: { token: string }) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
import type { DateRange } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import { IntervalTree, ContainmentSearchAlgorithm, createIntervalNodes } from "@calcom/lib/intervalTree";
|
import { IntervalTree, ContainmentSearchAlgorithm, createIntervalNodes } from "@calcom/lib/intervalTree";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
import type { DateRange } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
|
|
||||||
import { mergeOverlappingDateRanges } from "./mergeOverlappingDateRanges";
|
import { mergeOverlappingDateRanges } from "./mergeOverlappingDateRanges";
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
import type { DateRange } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
|
|
||||||
export function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
export function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
||||||
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
|
import type { DateRange } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
|
import { intersect } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
|
import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
|
||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
|
||||||
import { intersect } from "@calcom/lib/date-ranges";
|
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { filterRedundantDateRanges } from "./getAggregatedAvailability/date-range-utils/filterRedundantDateRanges";
|
import { filterRedundantDateRanges } from "./date-range-utils/filterRedundantDateRanges";
|
||||||
import { mergeOverlappingDateRanges } from "./getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges";
|
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
|
||||||
|
|
||||||
function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] {
|
function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
||||||
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
|
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
|
||||||
import type { IRedisService } from "@calcom/features/redis/IRedisService";
|
import type { IRedisService } from "@calcom/features/redis/IRedisService";
|
||||||
|
import type { DateOverride, WorkingHours } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
|
import { buildDateRanges, subtract } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
import type { DateOverride, WorkingHours } from "@calcom/lib/date-ranges";
|
|
||||||
import { buildDateRanges, subtract } from "@calcom/lib/date-ranges";
|
|
||||||
import { stringToDayjsZod } from "@calcom/lib/dayjs";
|
import { stringToDayjsZod } from "@calcom/lib/dayjs";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -641,9 +641,9 @@ export class UserAvailabilityService {
|
|||||||
// you can obtain that from user availability defined outside of here
|
// you can obtain that from user availability defined outside of here
|
||||||
fromUser: { id: user.id, displayName: user.name },
|
fromUser: { id: user.id, displayName: user.name },
|
||||||
// optional chaining destructuring toUser
|
// optional chaining destructuring toUser
|
||||||
toUser: !!toUser ? { id: toUser.id, displayName: toUser.name, username: toUser.username } : null,
|
toUser: toUser ? { id: toUser.id, displayName: toUser.name, username: toUser.username } : null,
|
||||||
reason: !!reason ? reason.reason : null,
|
reason: reason ? reason.reason : null,
|
||||||
emoji: !!reason ? reason.emoji : null,
|
emoji: reason ? reason.emoji : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocati
|
|||||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
|
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
|
||||||
import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes";
|
import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes";
|
||||||
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
||||||
import { SystemField } from "@calcom/lib/SystemField";
|
import { SystemField } from "@calcom/lib/bookings/SystemField";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
getPiiFreeCalendarEvent,
|
getPiiFreeCalendarEvent,
|
||||||
} from "@calcom/lib/piiFreeData";
|
} from "@calcom/lib/piiFreeData";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { DestinationCalendar, BookingReference } from "@calcom/prisma/client";
|
import type { DestinationCalendar, BookingReference } from "@calcom/prisma/client";
|
||||||
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
|
|
||||||
import logger from "../logger";
|
|
||||||
|
|
||||||
type BookingEventType = {
|
type BookingEventType = {
|
||||||
slug: string;
|
slug: string;
|
||||||
team: {
|
team: {
|
||||||
@@ -3,7 +3,7 @@ import type { z } from "zod";
|
|||||||
import {
|
import {
|
||||||
SMS_REMINDER_NUMBER_FIELD,
|
SMS_REMINDER_NUMBER_FIELD,
|
||||||
CAL_AI_AGENT_PHONE_NUMBER_FIELD,
|
CAL_AI_AGENT_PHONE_NUMBER_FIELD,
|
||||||
} from "@calcom/lib/SystemField";
|
} from "@calcom/lib/bookings/SystemField";
|
||||||
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
||||||
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
||||||
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
|
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import type { ALL_VIEWS } from "@calcom/features/form-builder/schema";
|
import type { ALL_VIEWS } from "@calcom/features/form-builder/schema";
|
||||||
import { dbReadResponseSchema, fieldTypesSchemaMap } from "@calcom/features/form-builder/schema";
|
import { fieldTypesSchemaMap } from "@calcom/features/form-builder/schema";
|
||||||
|
import { dbReadResponseSchema } from "@calcom/lib/dbReadResponseSchema";
|
||||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils";
|
import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import { SystemField } from "@calcom/lib/SystemField";
|
import { SystemField } from "@calcom/lib/bookings/SystemField";
|
||||||
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
import { contructEmailFromPhoneNumber } from "@calcom/lib/contructEmailFromPhoneNumber";
|
import { contructEmailFromPhoneNumber } from "@calcom/lib/contructEmailFromPhoneNumber";
|
||||||
import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types
|
|||||||
import { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
import { zodRoutes } from "@calcom/app-store/routing-forms/zod";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager";
|
import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager";
|
||||||
import { mergeOverlappingRanges } from "@calcom/lib/date-ranges";
|
import { mergeOverlappingRanges } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { raqbQueryValueSchema } from "@calcom/lib/raqb/zod";
|
import { raqbQueryValueSchema } from "@calcom/lib/raqb/zod";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
|||||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
import { WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
|||||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
|||||||
import type { PrismaAttributeRepository as AttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
import type { PrismaAttributeRepository as AttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||||
import type { HostRepository } from "@calcom/lib/server/repository/host";
|
import type { HostRepository } from "@calcom/lib/server/repository/host";
|
||||||
import type { PrismaOOORepository as OooRepository } from "@calcom/lib/server/repository/ooo";
|
import type { PrismaOOORepository as OooRepository } from "@calcom/lib/server/repository/ooo";
|
||||||
import { HashedLinkService } from "@calcom/lib/server/service/hashedLinkService";
|
import { HashedLinkService } from "@calcom/features/hashedLink/services/hashedLinkService";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { DestinationCalendar, Prisma, User, AssignmentReasonEnum } from "@calcom/prisma/client";
|
import type { DestinationCalendar, Prisma, User, AssignmentReasonEnum } from "@calcom/prisma/client";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Logger } from "tslog";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
import { checkForConflicts } from "@calcom/features/bookings/lib/conflictChecker/checkForConflicts";
|
||||||
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
import { buildDateRanges } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
|
||||||
import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability";
|
import { getUserAvailabilityService } from "@calcom/features/di/containers/GetUserAvailability";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhoo
|
|||||||
import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload";
|
import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
|||||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
|
|
||||||
import { handleNoShowFee } from "./handleNoShowFee";
|
import { handleNoShowFee } from "./handleNoShowFee";
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ vi.mock("@calcom/lib/server/i18n", () => ({
|
|||||||
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
|
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/server/repository/credential", () => ({
|
vi.mock("@calcom/features/credentials/repositories/CredentialRepository", () => ({
|
||||||
CredentialRepository: {
|
CredentialRepository: {
|
||||||
findPaymentCredentialByAppIdAndUserIdOrTeamId: vi.fn(),
|
findPaymentCredentialByAppIdAndUserIdOrTeamId: vi.fn(),
|
||||||
findPaymentCredentialByAppIdAndTeamId: vi.fn(),
|
findPaymentCredentialByAppIdAndTeamId: vi.fn(),
|
||||||
@@ -43,7 +43,7 @@ vi.mock("@calcom/features/membership/repositories/MembershipRepository", () => (
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@calcom/lib/server/repository/team", () => ({
|
vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({
|
||||||
TeamRepository: vi.fn().mockImplementation(() => ({
|
TeamRepository: vi.fn().mockImplementation(() => ({
|
||||||
findParentOrganizationByTeamId: vi.fn(),
|
findParentOrganizationByTeamId: vi.fn(),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
|
|||||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager";
|
import { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager";
|
||||||
import { getDefinedBufferTimes } from "@calcom/features/eventtypes/lib/getDefinedBufferTimes";
|
import { getDefinedBufferTimes } from "@calcom/features/eventtypes/lib/getDefinedBufferTimes";
|
||||||
import { subtract } from "@calcom/lib/date-ranges";
|
import { subtract } from "@calcom/features/schedules/lib/date-ranges";
|
||||||
import { stringToDayjs } from "@calcom/lib/dayjs";
|
import { stringToDayjs } from "@calcom/lib/dayjs";
|
||||||
import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
|
import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
|
||||||
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
||||||
|
|||||||
+18
-4
@@ -3,6 +3,7 @@ import { v4 as uuid } from "uuid";
|
|||||||
|
|
||||||
import { CalendarAuth } from "@calcom/app-store/googlecalendar/lib/CalendarAuth";
|
import { CalendarAuth } from "@calcom/app-store/googlecalendar/lib/CalendarAuth";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -117,12 +118,12 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!syncToken) {
|
if (!syncToken) {
|
||||||
const now = dayjs();
|
const now = dayjs().startOf("day");
|
||||||
// first sync or unsync (3 months)
|
// first sync or unsync (3 months)
|
||||||
const threeMonths = now.add(3, "month");
|
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
|
||||||
|
|
||||||
const timeMinISO = now.toISOString();
|
const timeMinISO = now.toISOString();
|
||||||
const timeMaxISO = threeMonths.toISOString();
|
const timeMaxISO = monthsAhead.toISOString();
|
||||||
params.timeMin = timeMinISO;
|
params.timeMin = timeMinISO;
|
||||||
params.timeMax = timeMaxISO;
|
params.timeMax = timeMaxISO;
|
||||||
} else {
|
} else {
|
||||||
@@ -151,8 +152,21 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseEvents(events: calendar_v3.Schema$Event[]): CalendarSubscriptionEventItem[] {
|
private parseEvents(events: calendar_v3.Schema$Event[]): CalendarSubscriptionEventItem[] {
|
||||||
|
const now = dayjs().startOf("day");
|
||||||
|
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
|
||||||
|
|
||||||
|
function filterEventsWithoutId(event: calendar_v3.Schema$Event) {
|
||||||
|
return typeof event.id === "string" && !!event.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterEventsByDateRange(event: calendar_v3.Schema$Event) {
|
||||||
|
const start = dayjs(event.start?.dateTime || event.start?.date);
|
||||||
|
return !start.isBefore(now) && !start.isAfter(monthsAhead);
|
||||||
|
}
|
||||||
|
|
||||||
return events
|
return events
|
||||||
.filter((event) => typeof event.id === "string" && !!event.id)
|
.filter(filterEventsWithoutId)
|
||||||
|
.filter(filterEventsByDateRange)
|
||||||
.map((event) => {
|
.map((event) => {
|
||||||
// empty or opaque is busy
|
// empty or opaque is busy
|
||||||
const busy = !event.transparency || event.transparency === "opaque";
|
const busy = !event.transparency || event.transparency === "opaque";
|
||||||
|
|||||||
+74
-50
@@ -2,11 +2,25 @@ import "../__mocks__/CalendarAuth";
|
|||||||
|
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||||
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||||
|
|
||||||
import { GoogleCalendarSubscriptionAdapter } from "../GoogleCalendarSubscription.adapter";
|
import { GoogleCalendarSubscriptionAdapter } from "../GoogleCalendarSubscription.adapter";
|
||||||
|
|
||||||
|
const addMonthsFromNow = (months: number) => {
|
||||||
|
const date = dayjs();
|
||||||
|
return date.add(months, "months").startOf("day");
|
||||||
|
};
|
||||||
|
|
||||||
|
const today = dayjs().startOf("day");
|
||||||
|
|
||||||
|
const oneWeekFromNow = today.add(7, "days");
|
||||||
|
|
||||||
|
const eventEndTime = oneWeekFromNow.add(1, "hours");
|
||||||
|
|
||||||
|
const channelExpirationDate = new Date(Date.now() + 86400000);
|
||||||
|
|
||||||
vi.mock("uuid", () => ({
|
vi.mock("uuid", () => ({
|
||||||
v4: vi.fn().mockReturnValue("test-uuid"),
|
v4: vi.fn().mockReturnValue("test-uuid"),
|
||||||
}));
|
}));
|
||||||
@@ -30,16 +44,16 @@ const mockSelectedCalendar: SelectedCalendar = {
|
|||||||
watchAttempts: 0,
|
watchAttempts: 0,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
unwatchAttempts: 0,
|
unwatchAttempts: 0,
|
||||||
createdAt: new Date(),
|
createdAt: today.toDate(),
|
||||||
updatedAt: new Date(),
|
updatedAt: today.toDate(),
|
||||||
channelId: "test-channel-id",
|
channelId: "test-channel-id",
|
||||||
channelKind: "web_hook",
|
channelKind: "web_hook",
|
||||||
channelResourceId: "test-resource-id",
|
channelResourceId: "test-resource-id",
|
||||||
channelResourceUri: "test-resource-uri",
|
channelResourceUri: "test-resource-uri",
|
||||||
channelExpiration: new Date(Date.now() + 86400000),
|
channelExpiration: channelExpirationDate,
|
||||||
syncSubscribedAt: new Date(),
|
syncSubscribedAt: today.toDate(),
|
||||||
syncToken: "test-sync-token",
|
syncToken: "test-sync-token",
|
||||||
syncedAt: new Date(),
|
syncedAt: today.toDate(),
|
||||||
syncErrorAt: null,
|
syncErrorAt: null,
|
||||||
syncErrorCount: 0,
|
syncErrorCount: 0,
|
||||||
};
|
};
|
||||||
@@ -176,7 +190,7 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
id: "test-channel-id",
|
id: "test-channel-id",
|
||||||
resourceId: "test-resource-id",
|
resourceId: "test-resource-id",
|
||||||
resourceUri: "test-resource-uri",
|
resourceUri: "test-resource-uri",
|
||||||
expiration: "1640995200000",
|
expiration: String(channelExpirationDate.getTime()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,17 +216,18 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
id: "test-channel-id",
|
id: "test-channel-id",
|
||||||
resourceId: "test-resource-id",
|
resourceId: "test-resource-id",
|
||||||
resourceUri: "test-resource-uri",
|
resourceUri: "test-resource-uri",
|
||||||
expiration: new Date(1640995200000),
|
expiration: channelExpirationDate,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle expiration as ISO string", async () => {
|
test("should handle expiration as ISO string", async () => {
|
||||||
|
const mockExpirationDate = new Date(Date.now() + 10000000);
|
||||||
const mockWatchResponse = {
|
const mockWatchResponse = {
|
||||||
data: {
|
data: {
|
||||||
id: "test-channel-id",
|
id: "test-channel-id",
|
||||||
resourceId: "test-resource-id",
|
resourceId: "test-resource-id",
|
||||||
resourceUri: "test-resource-uri",
|
resourceUri: "test-resource-uri",
|
||||||
expiration: "2023-12-01T10:00:00Z",
|
expiration: mockExpirationDate.toISOString(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,7 +235,7 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
|
|
||||||
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
|
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
|
||||||
|
|
||||||
expect(result.expiration).toEqual(new Date("2023-12-01T10:00:00Z"));
|
expect(result.expiration).toEqual(mockExpirationDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle missing expiration", async () => {
|
test("should handle missing expiration", async () => {
|
||||||
@@ -265,6 +280,19 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("fetchEvents", () => {
|
describe("fetchEvents", () => {
|
||||||
|
const commonEventData = {
|
||||||
|
iCalUID: "event-1@cal.com",
|
||||||
|
summary: "Test Event",
|
||||||
|
description: "Test Description",
|
||||||
|
location: "Test Location",
|
||||||
|
status: "confirmed",
|
||||||
|
transparency: "opaque",
|
||||||
|
kind: "calendar#event",
|
||||||
|
etag: "test-etag",
|
||||||
|
created: today.toISOString(),
|
||||||
|
updated: today.toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
test("should fetch events with sync token", async () => {
|
test("should fetch events with sync token", async () => {
|
||||||
const mockEventsResponse = {
|
const mockEventsResponse = {
|
||||||
data: {
|
data: {
|
||||||
@@ -272,23 +300,14 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
iCalUID: "event-1@cal.com",
|
...commonEventData,
|
||||||
summary: "Test Event",
|
|
||||||
description: "Test Description",
|
|
||||||
location: "Test Location",
|
|
||||||
start: {
|
start: {
|
||||||
dateTime: "2023-12-01T10:00:00Z",
|
dateTime: oneWeekFromNow.toISOString(),
|
||||||
timeZone: "UTC",
|
timeZone: "UTC",
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: "2023-12-01T11:00:00Z",
|
dateTime: eventEndTime.toISOString(),
|
||||||
},
|
},
|
||||||
status: "confirmed",
|
|
||||||
transparency: "opaque",
|
|
||||||
kind: "calendar#event",
|
|
||||||
etag: "test-etag",
|
|
||||||
created: "2023-12-01T09:00:00Z",
|
|
||||||
updated: "2023-12-01T09:30:00Z",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -312,8 +331,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
iCalUID: "event-1@cal.com",
|
iCalUID: "event-1@cal.com",
|
||||||
start: new Date("2023-12-01T10:00:00Z"),
|
start: oneWeekFromNow.toDate(),
|
||||||
end: new Date("2023-12-01T11:00:00Z"),
|
end: eventEndTime.toDate(),
|
||||||
busy: true,
|
busy: true,
|
||||||
summary: "Test Event",
|
summary: "Test Event",
|
||||||
description: "Test Description",
|
description: "Test Description",
|
||||||
@@ -325,14 +344,14 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
timeZone: "UTC",
|
timeZone: "UTC",
|
||||||
recurringEventId: null,
|
recurringEventId: null,
|
||||||
originalStartDate: null,
|
originalStartDate: null,
|
||||||
createdAt: new Date("2023-12-01T09:00:00Z"),
|
createdAt: today.toDate(),
|
||||||
updatedAt: new Date("2023-12-01T09:30:00Z"),
|
updatedAt: today.toDate(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should fetch events without sync token (initial sync)", async () => {
|
test("should fetch events without sync token (initial sync) with a 3-month range", async () => {
|
||||||
const calendarWithoutSyncToken = {
|
const calendarWithoutSyncToken = {
|
||||||
...mockSelectedCalendar,
|
...mockSelectedCalendar,
|
||||||
syncToken: null,
|
syncToken: null,
|
||||||
@@ -349,34 +368,37 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
|
|
||||||
const result = await adapter.fetchEvents(calendarWithoutSyncToken, mockCredential);
|
const result = await adapter.fetchEvents(calendarWithoutSyncToken, mockCredential);
|
||||||
|
|
||||||
|
const expectedTimeMin = today.startOf("day").toISOString();
|
||||||
|
const expectedTimeMax = addMonthsFromNow(3).endOf("day").toISOString();
|
||||||
|
|
||||||
expect(mockClient.events.list).toHaveBeenCalledWith({
|
expect(mockClient.events.list).toHaveBeenCalledWith({
|
||||||
calendarId: "test@example.com",
|
calendarId: "test@example.com",
|
||||||
pageToken: undefined,
|
pageToken: undefined,
|
||||||
singleEvents: true,
|
singleEvents: true,
|
||||||
timeMin: expect.any(String),
|
timeMin: expectedTimeMin,
|
||||||
timeMax: expect.any(String),
|
timeMax: expectedTimeMax,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.syncToken).toBe("initial-sync-token");
|
expect(result.syncToken).toBe("initial-sync-token");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle all-day events", async () => {
|
test("should handle all-day events", async () => {
|
||||||
|
const allDayStart = oneWeekFromNow.startOf("day");
|
||||||
|
const allDayEnd = oneWeekFromNow.endOf("day");
|
||||||
|
|
||||||
const mockEventsResponse = {
|
const mockEventsResponse = {
|
||||||
data: {
|
data: {
|
||||||
nextSyncToken: "new-sync-token",
|
nextSyncToken: "new-sync-token",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
iCalUID: "event-1@cal.com",
|
...commonEventData,
|
||||||
summary: "All Day Event",
|
|
||||||
start: {
|
start: {
|
||||||
date: "2023-12-01",
|
date: allDayStart.toISOString().split("T")[0],
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
date: "2023-12-02",
|
date: allDayEnd.toISOString().split("T")[0],
|
||||||
},
|
},
|
||||||
status: "confirmed",
|
|
||||||
transparency: "opaque",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -387,8 +409,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
|
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
|
||||||
|
|
||||||
expect(result.items[0].isAllDay).toBe(true);
|
expect(result.items[0].isAllDay).toBe(true);
|
||||||
expect(result.items[0].start).toEqual(new Date("2023-12-01"));
|
expect(result.items[0].start).toEqual(new Date(allDayStart.toISOString().split("T")[0]));
|
||||||
expect(result.items[0].end).toEqual(new Date("2023-12-02"));
|
expect(result.items[0].end).toEqual(new Date(allDayEnd.toISOString().split("T")[0]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle free events (transparent)", async () => {
|
test("should handle free events (transparent)", async () => {
|
||||||
@@ -398,15 +420,14 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
iCalUID: "event-1@cal.com",
|
...commonEventData,
|
||||||
summary: "Free Event",
|
summary: "Free Event",
|
||||||
start: {
|
start: {
|
||||||
dateTime: "2023-12-01T10:00:00Z",
|
dateTime: oneWeekFromNow.toISOString(),
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
dateTime: "2023-12-01T11:00:00Z",
|
dateTime: eventEndTime.toISOString(),
|
||||||
},
|
},
|
||||||
status: "confirmed",
|
|
||||||
transparency: "transparent",
|
transparency: "transparent",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -421,6 +442,9 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should handle pagination", async () => {
|
test("should handle pagination", async () => {
|
||||||
|
const date2 = new Date(oneWeekFromNow.toDate());
|
||||||
|
date2.setDate(date2.getDate() + 1);
|
||||||
|
|
||||||
const mockEventsResponse1 = {
|
const mockEventsResponse1 = {
|
||||||
data: {
|
data: {
|
||||||
nextPageToken: "page-2",
|
nextPageToken: "page-2",
|
||||||
@@ -428,8 +452,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
summary: "Event 1",
|
summary: "Event 1",
|
||||||
start: { dateTime: "2023-12-01T10:00:00Z" },
|
start: { dateTime: oneWeekFromNow.toISOString() },
|
||||||
end: { dateTime: "2023-12-01T11:00:00Z" },
|
end: { dateTime: eventEndTime.toISOString() },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -442,8 +466,8 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
{
|
{
|
||||||
id: "event-2",
|
id: "event-2",
|
||||||
summary: "Event 2",
|
summary: "Event 2",
|
||||||
start: { dateTime: "2023-12-01T12:00:00Z" },
|
start: { dateTime: date2.toISOString() },
|
||||||
end: { dateTime: "2023-12-01T13:00:00Z" },
|
end: { dateTime: new Date(date2.getTime() + 3600000).toISOString() },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -468,19 +492,19 @@ describe("GoogleCalendarSubscriptionAdapter", () => {
|
|||||||
{
|
{
|
||||||
id: "event-1",
|
id: "event-1",
|
||||||
summary: "Valid Event",
|
summary: "Valid Event",
|
||||||
start: { dateTime: "2023-12-01T10:00:00Z" },
|
start: { dateTime: oneWeekFromNow.toISOString() },
|
||||||
end: { dateTime: "2023-12-01T11:00:00Z" },
|
end: { dateTime: eventEndTime.toISOString() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
summary: "Invalid Event",
|
summary: "Invalid Event",
|
||||||
start: { dateTime: "2023-12-01T12:00:00Z" },
|
start: { dateTime: oneWeekFromNow.toISOString() },
|
||||||
end: { dateTime: "2023-12-01T13:00:00Z" },
|
end: { dateTime: eventEndTime.toISOString() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "",
|
id: "",
|
||||||
summary: "Empty ID Event",
|
summary: "Empty ID Event",
|
||||||
start: { dateTime: "2023-12-01T14:00:00Z" },
|
start: { dateTime: oneWeekFromNow.toISOString() },
|
||||||
end: { dateTime: "2023-12-01T15:00:00Z" },
|
end: { dateTime: eventEndTime.toISOString() },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-3
@@ -83,9 +83,7 @@ export class CalendarCacheEventRepository implements ICalendarCacheEventReposito
|
|||||||
async deleteStale() {
|
async deleteStale() {
|
||||||
return this.prismaClient.calendarCacheEvent.deleteMany({
|
return this.prismaClient.calendarCacheEvent.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
end: {
|
end: { lte: new Date() },
|
||||||
lte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -9,6 +9,7 @@ const log = logger.getSubLogger({ prefix: ["CalendarCacheEventService"] });
|
|||||||
* Service to handle calendar cache
|
* Service to handle calendar cache
|
||||||
*/
|
*/
|
||||||
export class CalendarCacheEventService {
|
export class CalendarCacheEventService {
|
||||||
|
static MONTHS_AHEAD: number = 3;
|
||||||
constructor(
|
constructor(
|
||||||
private deps: {
|
private deps: {
|
||||||
calendarCacheEventRepository: ICalendarCacheEventRepository;
|
calendarCacheEventRepository: ICalendarCacheEventRepository;
|
||||||
|
|||||||
Vendored
+1
-3
@@ -239,9 +239,7 @@ describe("CalendarCacheEventRepository", () => {
|
|||||||
|
|
||||||
expect(mockPrismaClient.calendarCacheEvent.deleteMany).toHaveBeenCalledWith({
|
expect(mockPrismaClient.calendarCacheEvent.deleteMany).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
end: {
|
end: { lte: expect.any(Date) },
|
||||||
lte: expect.any(Date),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user