Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4d0a6aa3 | ||
|
|
8e0cdf3671 | ||
|
|
fcd9480f9f | ||
|
|
09ddbe886f | ||
|
|
4ac8a51bc2 | ||
|
|
82951cd04e | ||
|
|
ae87fdcc9f | ||
|
|
35e29c4bee | ||
|
|
387a1704f3 | ||
|
|
3fb8c5bbc5 | ||
|
|
626585f5d0 | ||
|
|
4d3727db12 | ||
|
|
f6641d5173 | ||
|
|
f9b21206a2 | ||
|
|
96821c0026 | ||
|
|
3d5e4754af | ||
|
|
613e9f0788 | ||
|
|
1261247e94 | ||
|
|
2ad843ffbb | ||
|
|
6923b97cd2 |
@@ -26,7 +26,7 @@ runs:
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
- run: USE_POOL=true yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run generate-schemas &&
|
||||
rm -rf apps/api/v2/node_modules &&
|
||||
yarn install &&
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -16,7 +16,6 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
|
||||
@@ -10,7 +10,6 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
@@ -36,7 +35,6 @@ env:
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web App
|
||||
|
||||
@@ -67,6 +67,9 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Prisma generated files
|
||||
packages/prisma/client/*
|
||||
!packages/prisma/client/index.ts
|
||||
apps/api/v2/generated/prisma/*
|
||||
packages/prisma/generated/prisma/*
|
||||
packages/prisma/zod/**/*.ts
|
||||
packages/prisma/enums
|
||||
|
||||
|
||||
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
@@ -27,7 +27,7 @@ const getAllAdminMemberships = async (userId: number) => {
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
@@ -56,16 +56,19 @@ export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const orConditions = [];
|
||||
if (memberUserIds.length > 0) {
|
||||
orConditions.push({ userId: { in: memberUserIds } });
|
||||
}
|
||||
orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
OR: orConditions,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ async function authMiddleware(req: NextApiRequest) {
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
where: { id, booking: { userId }, deleted: null },
|
||||
});
|
||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.bookingReference.delete({ where: { id } });
|
||||
await prisma.bookingReference.update({ where: { id }, data: { deleted: true } });
|
||||
return { message: `BookingReference with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
||||
* 404:
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
export async function handler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
? { where: { deleted: null } }
|
||||
: { where: { booking: { userId }, deleted: null } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
export default defaultResponder(handler);
|
||||
|
||||
@@ -3,8 +3,8 @@ import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
@@ -48,7 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
: availableSlots.slots;
|
||||
|
||||
return { slots: slotsInProvidedTimeZone };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking, Credential, EventType, User } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { patchHandler } from "../../pages/api/booking-references/[id]/_patch";
|
||||
import { handler } from "../../pages/api/booking-references/_get";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("GET /api/booking-references - Soft-delete filtering", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-test@example.com",
|
||||
username: "api-bookingreference-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-test-booking-uid",
|
||||
title: "API Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should only return active booking references, excluding soft-deleted ones", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-active-ref-${timestamp}`,
|
||||
meetingId: `api-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-deleted-ref-${timestamp}`,
|
||||
meetingId: `api-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.booking_references).toBeDefined();
|
||||
expect(Array.isArray(responseData.booking_references)).toBe(true);
|
||||
|
||||
const returnedActiveRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(returnedActiveRef).toBeDefined();
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should filter booking references by user when not system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const otherUser = await prisma.user.create({
|
||||
data: {
|
||||
email: `other-user-bookingreference-${timestamp}@example.com`,
|
||||
username: `other-user-bookingreference-${timestamp}`,
|
||||
},
|
||||
});
|
||||
|
||||
const otherBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `other-user-booking-uid-${timestamp}`,
|
||||
title: "Other User Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: otherUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
const otherUserRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `other-user-ref-${timestamp}`,
|
||||
meetingId: `other-user-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: otherBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(otherUserRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const userRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === otherUserRef.id
|
||||
);
|
||||
|
||||
expect(userRefs).toHaveLength(0);
|
||||
|
||||
await prisma.booking.delete({ where: { id: otherBooking.id } });
|
||||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||||
});
|
||||
|
||||
it("should return only active booking references for user, not soft-deleted ones for system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef1 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-1-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-1-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef1.id);
|
||||
|
||||
const activeRef2 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-2-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-2-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef2.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-deleted-ref-${timestamp}`,
|
||||
meetingId: `admin-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const activeRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(activeRefs.length).toBe(2);
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should verify soft-deleted references are never returned through the API", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "office365_calendar",
|
||||
uid: `never-return-active-${timestamp}`,
|
||||
meetingId: `never-return-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const softDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
|
||||
expect(softDeletedRef).toBeUndefined();
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb).toBeDefined();
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/booking-references/[id] - Existing functionality", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-patch-test@example.com",
|
||||
username: "api-bookingreference-patch-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-patch-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-patch-test-booking-uid",
|
||||
title: "API Patch Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully update an active booking reference", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-active-ref-${timestamp}`,
|
||||
meetingId: `patch-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "new-password-123",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(activeRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("new-password-123");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("new-password-123");
|
||||
});
|
||||
|
||||
it("should allow updating a soft-deleted booking reference (existing functionality)", async () => {
|
||||
const timestamp = Date.now();
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-deleted-ref-${timestamp}`,
|
||||
meetingId: `patch-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: deletedRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(deletedRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: deletedRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("updated-password");
|
||||
expect(updatedRef?.deleted).toBe(true);
|
||||
});
|
||||
|
||||
it("should verify that booking references can be updated regardless of deleted status", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `patch-verify-active-${timestamp}`,
|
||||
meetingId: `patch-verify-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb?.meetingPassword).toBe("updated-password");
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -42,4 +42,5 @@ lerna-debug.log*
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.appStore.example
|
||||
/client/*
|
||||
/client/*
|
||||
/generated/prisma/*
|
||||
|
||||
@@ -22,6 +22,10 @@ const loadConfig = (): AppConfig => {
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
writeUrl: getEnv("DATABASE_WRITE_URL"),
|
||||
readPoolMax: getEnv("DATABASE_READ_POOL_MAX", 9),
|
||||
writePoolMax: getEnv("DATABASE_WRITE_POOL_MAX", 7),
|
||||
workerReadPoolMax: getEnv("DATABASE_READ_WORKER_POOL_MAX", 4),
|
||||
workerWritePoolMax: getEnv("DATABASE_WRITE_WORKER_POOL_MAX", 6),
|
||||
redisUrl: getEnv("REDIS_URL"),
|
||||
},
|
||||
next: {
|
||||
|
||||
@@ -13,6 +13,10 @@ export type AppConfig = {
|
||||
db: {
|
||||
readUrl: string;
|
||||
writeUrl: string;
|
||||
readPoolMax: number;
|
||||
writePoolMax: number;
|
||||
workerReadPoolMax: number;
|
||||
workerWritePoolMax: number;
|
||||
redisUrl: string;
|
||||
};
|
||||
next: {
|
||||
|
||||
@@ -9,7 +9,7 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
|
||||
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId, deleted: null };
|
||||
|
||||
if (filter?.type) {
|
||||
whereClause.type = filter.type;
|
||||
@@ -30,6 +30,7 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
return this.dbRead.prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
uid: eventUid,
|
||||
deleted: null,
|
||||
},
|
||||
include: {
|
||||
credential: true,
|
||||
|
||||
+3
-1
@@ -39,9 +39,11 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const { eventType: eventTypeCreated } = await createEventType({
|
||||
input: rest,
|
||||
ctx: {
|
||||
|
||||
@@ -34,6 +34,10 @@ export type Environment = {
|
||||
IS_TEAM_BILLING_ENABLED: boolean;
|
||||
// Used to enable/disable the rewrite of /api/v2 to /v2, active by default.
|
||||
REWRITE_API_V2_PREFIX: string;
|
||||
DATABASE_READ_POOL_MAX: number;
|
||||
DATABASE_WRITE_POOL_MAX: number;
|
||||
DATABASE_READ_WORKER_POOL_MAX: number;
|
||||
DATABASE_WRITE_WORKER_POOL_MAX: number;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -16,12 +16,12 @@ import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
import { UserAvailabilityService } from "@/lib/services/user-availability.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaWorkerModule],
|
||||
providers: [
|
||||
PrismaOOORepository,
|
||||
PrismaScheduleRepository,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository"
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -24,9 +25,10 @@ import { Module } from "@nestjs/common";
|
||||
CacheService,
|
||||
CheckBookingAndDurationLimitsService,
|
||||
CheckBookingLimitsService,
|
||||
HashedLinkService,
|
||||
LuckyUserService,
|
||||
RegularBookingService,
|
||||
],
|
||||
exports: [RegularBookingService],
|
||||
})
|
||||
export class RegularBookingModule {}
|
||||
export class RegularBookingModule { }
|
||||
|
||||
@@ -5,9 +5,7 @@ import { BookingCancelService as BaseBookingCancelService } from "@calcom/featur
|
||||
|
||||
@Injectable()
|
||||
export class BookingCancelService extends BaseBookingCancelService {
|
||||
constructor(
|
||||
prismaWriteService: PrismaWriteService,
|
||||
) {
|
||||
constructor(prismaWriteService: PrismaWriteService) {
|
||||
super({
|
||||
prismaClient: prismaWriteService.prisma,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { HashedLinkService as BaseHashedLinkService } from "@calcom/platform-libraries/private-links";
|
||||
|
||||
@Injectable()
|
||||
export class HashedLinkService extends BaseHashedLinkService {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repos
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
@@ -16,6 +17,7 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
|
||||
prismaWriteService: PrismaWriteService,
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
hashedLinkService: HashedLinkService,
|
||||
luckyUserService: LuckyUserService,
|
||||
userRepository: PrismaUserRepository
|
||||
) {
|
||||
@@ -24,6 +26,7 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
checkBookingAndDurationLimitsService,
|
||||
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
|
||||
bookingRepository,
|
||||
hashedLinkService,
|
||||
luckyUserService,
|
||||
userRepository,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import type { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { PlatformBilling, Team } from "@calcom/prisma/client";
|
||||
|
||||
export type BillingData = {
|
||||
|
||||
@@ -107,7 +107,7 @@ export class MembershipsRepository {
|
||||
},
|
||||
});
|
||||
|
||||
return userOrgAdminMemberships.map((m) => m.teamId);
|
||||
return userOrgAdminMemberships.map((m: { teamId: number }) => m.teamId);
|
||||
}
|
||||
|
||||
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { MembershipsRepository } from "@/modules/memberships/memberships.reposit
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { intersectionBy } from "lodash";
|
||||
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class MembershipsService {
|
||||
constructor(private readonly membershipsRepository: MembershipsRepository) {}
|
||||
@@ -16,8 +18,8 @@ export class MembershipsService {
|
||||
const secondUserMemberships = await this.membershipsRepository.findUserMemberships(secondUserId);
|
||||
|
||||
return intersectionBy(
|
||||
firstUserMemberships.filter((m) => m.accepted),
|
||||
secondUserMemberships.filter((m) => m.accepted),
|
||||
firstUserMemberships.filter((m: Membership) => m.accepted),
|
||||
secondUserMemberships.filter((m: Membership) => m.accepted),
|
||||
"teamId"
|
||||
);
|
||||
}
|
||||
|
||||
+13
-14
@@ -434,20 +434,19 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
|
||||
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.hosts).toEqual([
|
||||
{
|
||||
userId: teammate1.id,
|
||||
name: teammate1.name,
|
||||
username: teammate1.username,
|
||||
avatarUrl: teammate1.avatarUrl,
|
||||
},
|
||||
{
|
||||
userId: teammate2.id,
|
||||
name: teammate2.name,
|
||||
username: teammate2.username,
|
||||
avatarUrl: teammate2.avatarUrl,
|
||||
},
|
||||
]);
|
||||
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate1.id)).toEqual({
|
||||
userId: teammate1.id,
|
||||
name: teammate1.name,
|
||||
username: teammate1.username,
|
||||
avatarUrl: teammate1.avatarUrl,
|
||||
});
|
||||
|
||||
expect(responseTeamEvent?.hosts?.find((host) => host.userId === teammate2.id)).toEqual({
|
||||
userId: teammate2.id,
|
||||
name: teammate2.name,
|
||||
username: teammate2.username,
|
||||
avatarUrl: teammate2.avatarUrl,
|
||||
});
|
||||
|
||||
if (!responseTeamEvent) {
|
||||
throw new Error("Team event not found");
|
||||
|
||||
-3
@@ -122,7 +122,6 @@ describe("Organizations Teams Invite Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
@@ -131,5 +130,3 @@ describe("Organizations Teams Invite Endpoints", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
+5
-5
@@ -1,5 +1,9 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import {
|
||||
OPTIONAL_API_KEY_HEADER,
|
||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||
} from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
@@ -11,10 +15,8 @@ import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
|
||||
import { CreateInviteOutputDto } from "./outputs/invite.output";
|
||||
|
||||
@Controller({
|
||||
@@ -39,5 +41,3 @@ export class OrganizationsTeamsInviteController {
|
||||
return { status: SUCCESS_STATUS, data: result };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ export class InviteDataDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
|
||||
example: "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||
example:
|
||||
"http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||
})
|
||||
inviteLink!: string;
|
||||
}
|
||||
@@ -23,5 +24,3 @@ export class CreateInviteOutputDto {
|
||||
@ApiProperty({ type: InviteDataDto })
|
||||
data!: InviteDataDto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import type { Prisma, AttributeToUser, Membership, User, Profile } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsUsersRepository {
|
||||
@@ -60,7 +61,11 @@ export class OrganizationsUsersRepository {
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
return attributeToUsersWithProfile.map((attributeToUser) => attributeToUser.member.user);
|
||||
return attributeToUsersWithProfile.map(
|
||||
(
|
||||
attributeToUser: AttributeToUser & { member: Membership & { user: User & { profiles: Profile[] } } }
|
||||
) => attributeToUser.member.user
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganizationUsersByEmails(
|
||||
|
||||
@@ -1,36 +1,79 @@
|
||||
import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
const DB_MAX_POOL_CONNECTION = 10;
|
||||
|
||||
export interface PrismaServiceOptions {
|
||||
readUrl?: string;
|
||||
maxReadConnections?: number;
|
||||
e2e?: boolean;
|
||||
type: "main" | "worker";
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrismaReadService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaReadService");
|
||||
|
||||
public prisma: PrismaClient;
|
||||
public prisma!: PrismaClient;
|
||||
private pool!: Pool;
|
||||
private options!: PrismaServiceOptions;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||
constructor(private configService?: ConfigService) {
|
||||
if (configService) {
|
||||
// Use ConfigService defaults
|
||||
const readUrl = configService.get<string>("db.readUrl", { infer: true });
|
||||
const poolMax = parseInt(
|
||||
configService.get<number>("db.readPoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
|
||||
);
|
||||
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
this.setOptions({
|
||||
readUrl,
|
||||
maxReadConnections: poolMax,
|
||||
e2e,
|
||||
type: "main",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(options: PrismaServiceOptions) {
|
||||
this.options = options;
|
||||
|
||||
const dbUrl = options.readUrl;
|
||||
const isE2E = options.e2e ?? false;
|
||||
|
||||
this.pool = new Pool({
|
||||
connectionString: dbUrl,
|
||||
max: isE2E ? 1 : options.maxReadConnections ?? DB_MAX_POOL_CONNECTION,
|
||||
idleTimeoutMillis: 300000,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(this.pool);
|
||||
this.prisma = new PrismaClient({ adapter });
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
if (!this.prisma) return;
|
||||
|
||||
try {
|
||||
await this.prisma.$connect();
|
||||
this.logger.log("Connected to read database");
|
||||
} catch (error) {
|
||||
this.logger.error("Database connection failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
if (this.prisma) await this.prisma.$disconnect();
|
||||
if (this.pool) await this.pool.end();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error("Error disconnecting from read database", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrismaReadService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaReadService();
|
||||
service.setOptions({
|
||||
readUrl: config.get<string>("db.readUrl", { infer: true }),
|
||||
maxReadConnections: parseInt(config.get<number>("db.workerReadPoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "worker",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: PrismaWriteService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaWriteService();
|
||||
service.setOptions({
|
||||
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
|
||||
maxWriteConnections: parseInt(config.get<number>("db.workerWritePoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "worker",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrismaReadService, PrismaWriteService],
|
||||
})
|
||||
export class PrismaWorkerModule {}
|
||||
@@ -1,36 +1,75 @@
|
||||
import { OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { OnModuleDestroy, OnModuleInit, Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import { PrismaClient } from "@calcom/prisma/client";
|
||||
|
||||
const DB_MAX_POOL_CONNECTION = 5;
|
||||
|
||||
export interface PrismaServiceOptions {
|
||||
writeUrl?: string;
|
||||
maxWriteConnections?: number;
|
||||
e2e?: boolean;
|
||||
type: "main" | "worker";
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrismaWriteService implements OnModuleInit, OnModuleDestroy {
|
||||
private logger = new Logger("PrismaWriteService");
|
||||
public prisma!: PrismaClient;
|
||||
private pool!: Pool;
|
||||
private options!: PrismaServiceOptions;
|
||||
|
||||
public prisma: PrismaClient;
|
||||
constructor(private configService?: ConfigService) {
|
||||
if (configService) {
|
||||
const writeUrl = configService.get<string>("db.writeUrl", { infer: true });
|
||||
const poolMax = parseInt(
|
||||
configService.get<number>("db.writePoolMax", { infer: true }) ?? DB_MAX_POOL_CONNECTION
|
||||
);
|
||||
const e2e = configService.get<boolean>("e2e", { infer: true }) ?? false;
|
||||
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||
this.setOptions({
|
||||
writeUrl,
|
||||
maxWriteConnections: poolMax,
|
||||
e2e,
|
||||
type: "main",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: dbUrl,
|
||||
},
|
||||
},
|
||||
setOptions(options: PrismaServiceOptions) {
|
||||
this.options = options;
|
||||
|
||||
const dbUrl = options.writeUrl;
|
||||
const isE2E = options.e2e ?? false;
|
||||
this.pool = new Pool({
|
||||
connectionString: dbUrl,
|
||||
max: isE2E ? 1 : options.maxWriteConnections ?? DB_MAX_POOL_CONNECTION,
|
||||
idleTimeoutMillis: 300000,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(this.pool);
|
||||
this.prisma = new PrismaClient({ adapter });
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.prisma.$connect();
|
||||
if (!this.prisma) return;
|
||||
|
||||
try {
|
||||
await this.prisma.$connect();
|
||||
this.logger.log("Connected to write database");
|
||||
} catch (error) {
|
||||
this.logger.error("Database connection failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.prisma.$disconnect();
|
||||
if (this.prisma) await this.prisma.$disconnect();
|
||||
if (this.pool) await this.pool.end();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
this.logger.error("Error disconnecting from write database", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
providers: [PrismaReadService, PrismaWriteService],
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrismaReadService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaReadService();
|
||||
service.setOptions({
|
||||
readUrl: config.get<string>("db.readUrl", { infer: true }),
|
||||
maxReadConnections: parseInt(config.get<number>("db.readPoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "main",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
{
|
||||
provide: PrismaWriteService,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const service = new PrismaWriteService();
|
||||
service.setOptions({
|
||||
writeUrl: config.get<string>("db.writeUrl", { infer: true }),
|
||||
maxWriteConnections: parseInt(config.get<number>("db.writePoolMax", { infer: true })),
|
||||
e2e: config.get<boolean>("e2e", { infer: true }) ?? false,
|
||||
type: "main",
|
||||
});
|
||||
return service;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrismaReadService, PrismaWriteService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Needed if ConfigService is used anywhere
|
||||
import appConfig from "@/config/app";
|
||||
import { AvailableSlotsModule } from "@/lib/modules/available-slots.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { PrismaWorkerModule } from "@/modules/prisma/prisma-worker.module";
|
||||
import { Module, Logger } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { SentryModule } from "@sentry/nestjs/setup";
|
||||
@@ -15,7 +15,7 @@ import { SentryModule } from "@sentry/nestjs/setup";
|
||||
isGlobal: true,
|
||||
load: [appConfig],
|
||||
}),
|
||||
PrismaModule,
|
||||
PrismaWorkerModule,
|
||||
AvailableSlotsModule,
|
||||
],
|
||||
providers: [Logger],
|
||||
|
||||
@@ -118,7 +118,7 @@ export class StripeService {
|
||||
userId
|
||||
);
|
||||
|
||||
const credentialIdsToDelete = existingCredentials.map((item) => item.id);
|
||||
const credentialIdsToDelete = existingCredentials.map((item: Credential) => item.id);
|
||||
if (credentialIdsToDelete.length > 0) {
|
||||
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MembershipUserSelect: Prisma.UserSelect = {
|
||||
name: true,
|
||||
metadata: true,
|
||||
bio: true,
|
||||
};
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsRepository {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { teamMetadataSchema } from "@calcom/platform-libraries";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import type { Membership, Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsRepository {
|
||||
@@ -41,7 +41,7 @@ export class TeamsRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => member.userId);
|
||||
return teamMembers.map((member: Membership) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamManagedUsersIds(teamId: number) {
|
||||
@@ -57,7 +57,7 @@ export class TeamsRepository {
|
||||
return [];
|
||||
}
|
||||
|
||||
return teamMembers.map((member) => member.userId);
|
||||
return teamMembers.map((member: Membership) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamsUserIsMemberOf(userId: number) {
|
||||
|
||||
@@ -289,7 +289,7 @@ export class UsersRepository {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
return profiles.map((profile) => profile.user);
|
||||
return profiles.map((profile: Profile & { user: User }) => profile.user);
|
||||
}
|
||||
|
||||
async setDefaultConferencingApp(userId: number, appSlug?: string, appLink?: string) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@prisma/client/*": ["./client/*"],
|
||||
"@calcom/prisma/client": ["../../../packages/prisma/generated/prisma/client"],
|
||||
"@calcom/prisma/enums": ["../../../packages/prisma/enums"],
|
||||
"@calcom/platform-constants": ["../../../packages/platform/constants/index.ts"],
|
||||
"@calcom/platform-types": ["../../../packages/platform/types/index.ts"],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { unstable_cache } from "next/cache";
|
||||
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import {
|
||||
getEventTypeHosts,
|
||||
getProfileFromEvent,
|
||||
@@ -11,10 +12,9 @@ import {
|
||||
} from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventType";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -23,8 +23,14 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const searchParams = await _searchParams;
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
|
||||
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}${autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ''}` : null;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept)
|
||||
? searchParams.autoAccept[0]
|
||||
: searchParams?.autoAccept;
|
||||
const callbackUrl = token
|
||||
? `/teams?token=${encodeURIComponent(token)}${
|
||||
autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ""
|
||||
}`
|
||||
: null;
|
||||
|
||||
if (!session) {
|
||||
redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { Session } from "next-auth";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -33,7 +33,9 @@ export const ServerTeamsListing = async ({
|
||||
session: Session;
|
||||
}) => {
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
|
||||
const autoAccept = Array.isArray(searchParams?.autoAccept)
|
||||
? searchParams.autoAccept[0]
|
||||
: searchParams?.autoAccept;
|
||||
const userId = session.user.id;
|
||||
let invitationAccepted = false;
|
||||
|
||||
@@ -42,7 +44,7 @@ export const ServerTeamsListing = async ({
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
if (autoAccept === "true") {
|
||||
if (autoAccept === "true") {
|
||||
await TeamService.acceptInvitationByToken(token, userId);
|
||||
invitationAccepted = true;
|
||||
} else {
|
||||
|
||||
@@ -308,7 +308,7 @@ const WeightedAttributesSelector = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-muted mt-1 rounded-xl p-2">
|
||||
{!!attributeIdForWeights ? (
|
||||
{attributeIdForWeights ? (
|
||||
<SelectField
|
||||
size="sm"
|
||||
containerClassName="data-testid-select-router"
|
||||
|
||||
@@ -76,7 +76,6 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
hookForm.reset({});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const filters = getTeamsFiltersFromQuery(routerQuery);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OnboardingView } from "~/onboarding/getting-started/onboarding-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("getting_started")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/getting-started"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
// Hello {username} || there. Not sure how to do this nicely with i18n just yet
|
||||
const userName = session.user.name || "there";
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OnboardingView userName={userName} userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export default async function OnboardingLayout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const isOnboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
|
||||
|
||||
if (!isOnboardingV3Enabled) {
|
||||
redirect("/getting-started");
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationBrandView } from "~/onboarding/organization/brand/organization-brand-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("organization_brand")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/brand"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationBrandView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationDetailsView } from "~/onboarding/organization/details/organization-details-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("organization_details")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/details"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationDetailsView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationInviteView } from "~/onboarding/organization/invite/organization-invite-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("invite_teammates")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/invite"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationInviteView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { OrganizationTeamsView } from "~/onboarding/organization/teams/organization-teams-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${APP_NAME} - ${t("organization_teams")}`,
|
||||
() => "",
|
||||
true,
|
||||
undefined,
|
||||
"/onboarding/organization/teams"
|
||||
);
|
||||
};
|
||||
|
||||
const ServerPage = async () => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const userEmail = session.user.email || "";
|
||||
|
||||
return <OrganizationTeamsView userEmail={userEmail} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
+1
-1
@@ -4,8 +4,8 @@ import { z } from "zod";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { OrgForm } from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
const orgIdSchema = z.object({ id: z.coerce.number() });
|
||||
|
||||
|
||||
-2
@@ -369,7 +369,6 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
const tabMembers = Array.from(document.getElementsByTagName("a")).filter(
|
||||
(bottom) => bottom.dataset.testid === "vertical-tab-Members"
|
||||
)[1];
|
||||
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Settings layout isn't embedded
|
||||
tabMembers?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
}
|
||||
@@ -568,7 +567,6 @@ const SettingsSidebarContainer = ({
|
||||
const tabMembers = Array.from(document.getElementsByTagName("a")).filter(
|
||||
(bottom) => bottom.dataset.testid === "vertical-tab-Members"
|
||||
)[1];
|
||||
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Settings layout isn't embedded
|
||||
tabMembers?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,9 +2,9 @@ import type { PageProps } from "app/_types";
|
||||
import { getTranslate, _generateMetadata } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
|
||||
import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { WebhookRepository } from "@calcom/features/webhooks/lib/repository/WebhookRepository";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { notFound } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import type { AppFlags } from "@calcom/features/flags/config";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { OtherTeamsListing } from "@calcom/features/ee/organizations/pages/components/OtherTeamsListing";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { validateUserHasOrg } from "../../../actions/validateUserHasOrg";
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ const resolveEndpoint = (links: any) => {
|
||||
// to the correct API endpoints.
|
||||
// - viewer.me - 2 segment paths like this are for logged in requests
|
||||
// - viewer.public.i18n - 3 segments paths can be public or authed
|
||||
// - viewer.eventTypes.heavy.create - 4 segments paths for heavy sub-router
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (ctx: any) => {
|
||||
const parts = ctx.op.path.split(".");
|
||||
@@ -24,11 +23,7 @@ const resolveEndpoint = (links: any) => {
|
||||
if (parts.length == 2) {
|
||||
endpoint = parts[0] as keyof typeof links;
|
||||
path = parts[1];
|
||||
} else if (parts.length >= 3 && parts[2] === "heavy") {
|
||||
endpoint = parts[1] + "/heavy" as keyof typeof links;
|
||||
path = parts[3];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
endpoint = parts[1] as keyof typeof links;
|
||||
path = parts.splice(2, parts.length - 2).join(".");
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import {
|
||||
getCalendarCredentials,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -13,7 +13,7 @@ async function handler(req: NextRequest) {
|
||||
let appDirRequestBody;
|
||||
try {
|
||||
appDirRequestBody = await req.json();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return NextResponse.json({ success: false, message: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
|
||||
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
|
||||
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
|
||||
@@ -8,7 +9,6 @@ import { CalendarCacheEventService } from "@calcom/features/calendar-subscriptio
|
||||
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||
import { CalendarAppDelegationCredentialInvalidGrantError } from "@calcom/lib/CalendarAppError";
|
||||
import { findUniqueDelegationCalendarCredential } from "@calcom/app-store/delegationCredential";
|
||||
import GoogleCalendarService from "@calcom/app-store/googlecalendar/lib/CalendarService";
|
||||
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||
import { CalendarAppDelegationCredentialInvalidGrantError } from "@calcom/lib/CalendarAppError";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
sendDailyVideoRecordingEmails,
|
||||
sendDailyVideoTranscriptEmails,
|
||||
} from "@calcom/emails/daily-video-emails";
|
||||
import {
|
||||
getAllTranscriptsAccessLinkFromMeetingId,
|
||||
submitBatchProcessorTranscriptionJob,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import {
|
||||
getAllTranscriptsAccessLinkFromMeetingId,
|
||||
submitBatchProcessorTranscriptionJob,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { generateVideoToken } from "@calcom/lib/videoTokens";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { getBooking } from "@calcom/web/lib/daily-webhook/getBooking";
|
||||
|
||||
@@ -6,11 +6,11 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { BillingPlanService } from "@calcom/features/ee/billing/domain/billing-plans";
|
||||
import type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { intercom } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Params } from "app/_types";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import type { CalendarSubscriptionProvider } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
|
||||
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
|
||||
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
|
||||
@@ -11,7 +12,6 @@ import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
|
||||
|
||||
|
||||
@@ -696,7 +696,15 @@ describe("Retell AI Webhook Handler", () => {
|
||||
describe("Web Call Tests", () => {
|
||||
const mockAgent: Pick<
|
||||
Agent,
|
||||
"id" | "name" | "providerAgentId" | "enabled" | "userId" | "teamId" | "createdAt" | "updatedAt" | "inboundEventTypeId"
|
||||
| "id"
|
||||
| "name"
|
||||
| "providerAgentId"
|
||||
| "enabled"
|
||||
| "userId"
|
||||
| "teamId"
|
||||
| "createdAt"
|
||||
| "updatedAt"
|
||||
| "inboundEventTypeId"
|
||||
> = {
|
||||
id: "agent-123",
|
||||
name: "Test Agent",
|
||||
|
||||
Vendored
+1
-1
@@ -2,8 +2,8 @@
|
||||
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
|
||||
const CACHE_TAGS = {
|
||||
HAS_TEAM_PLAN: "MembershipRepository.findFirstAcceptedMembershipByUserId",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
|
||||
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -115,7 +115,9 @@ export function getEditEventActions(context: BookingActionContext): ActionType[]
|
||||
icon: "clock",
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length && isAttendee && seatReferenceUid ? `?seatReferenceUid=${seatReferenceUid}` : ""
|
||||
booking.seatsReferences.length && isAttendee && seatReferenceUid
|
||||
? `?seatReferenceUid=${seatReferenceUid}`
|
||||
: ""
|
||||
}`,
|
||||
disabled:
|
||||
(isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling,
|
||||
|
||||
@@ -187,7 +187,7 @@ const useReroutingState = ({ isOpenDialog }: Pick<RerouteDialogProps, "isOpenDia
|
||||
const status = (() => {
|
||||
if (!value) return ReroutingStatusEnum.REROUTING_NOT_INITIATED;
|
||||
if (value.error) return ReroutingStatusEnum.REROUTING_FAILED;
|
||||
if (!!value.newBooking) return ReroutingStatusEnum.REROUTING_COMPLETE;
|
||||
if (value.newBooking) return ReroutingStatusEnum.REROUTING_COMPLETE;
|
||||
return ReroutingStatusEnum.REROUTING_IN_PROGRESS;
|
||||
})();
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const UserProfile = ({ user }: UserProfileProps) => {
|
||||
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
||||
const utils = trpc.useUtils();
|
||||
const router = useRouter();
|
||||
const createEventType = trpc.viewer.eventTypes.heavy.create.useMutation();
|
||||
const createEventType = trpc.viewer.eventTypesHeavy.create.useMutation();
|
||||
const telemetry = useTelemetry();
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ThemeProvider } from "next-themes";
|
||||
import type { AppProps as NextAppProps } from "next/app";
|
||||
import type { ReadonlyURLSearchParams } from "next/navigation";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
@@ -119,15 +120,17 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
nonce={props.nonce}
|
||||
isThemeSupported={isThemeSupported}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
{props.isBookingPage || isBookingPage ? (
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
) : (
|
||||
<DynamicIntercomProvider>
|
||||
<NuqsAdapter>
|
||||
<FeatureFlagsProvider>
|
||||
{props.isBookingPage || isBookingPage ? (
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
</DynamicIntercomProvider>
|
||||
)}
|
||||
</FeatureFlagsProvider>
|
||||
) : (
|
||||
<DynamicIntercomProvider>
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
</DynamicIntercomProvider>
|
||||
)}
|
||||
</FeatureFlagsProvider>
|
||||
</NuqsAdapter>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
</EventCollectionProvider>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SSRConfig } from "next-i18next/dist/types/types";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
||||
import dynamic from "next/dynamic";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/pages";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
@@ -286,15 +287,17 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
isThemeSupported={props.Component.isThemeSupported}
|
||||
isBookingPage={props.Component.isBookingPage || isBookingPage}
|
||||
router={props.router}>
|
||||
<FeatureFlagsProvider>
|
||||
{_isBookingPage ? (
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
) : (
|
||||
<DynamicIntercomProvider>
|
||||
<NuqsAdapter>
|
||||
<FeatureFlagsProvider>
|
||||
{_isBookingPage ? (
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
</DynamicIntercomProvider>
|
||||
)}
|
||||
</FeatureFlagsProvider>
|
||||
) : (
|
||||
<DynamicIntercomProvider>
|
||||
<OrgBrandProvider>{props.children}</OrgBrandProvider>
|
||||
</DynamicIntercomProvider>
|
||||
)}
|
||||
</FeatureFlagsProvider>
|
||||
</NuqsAdapter>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
|
||||
@@ -6,10 +6,10 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
@@ -281,7 +281,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl,
|
||||
alreadyInstalled: appInstalls.some((install) => !Boolean(install.teamId) && install.userId === user.id),
|
||||
alreadyInstalled: appInstalls.some((install) => !install.teamId && install.userId === user.id),
|
||||
};
|
||||
|
||||
const teamsWithIsAppInstalled = hasTeams
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData";
|
||||
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { HashedLinkService } from "@calcom/features/hashedLink/services/hashedLinkService";
|
||||
import {
|
||||
shouldHideBrandingForTeamEvent,
|
||||
shouldHideBrandingForUserEvent,
|
||||
} from "@calcom/features/profile/lib/hideBranding";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { HashedLinkService } from "@calcom/features/hashedLink/lib/service/HashedLinkService";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/enums";
|
||||
@@ -82,9 +82,8 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
// App Router doesn't have access to the current path directly, so we build it manually
|
||||
destination: `${redirectWithOriginAndSearchString.origin ?? ""}/d/${link}/${slug}${
|
||||
redirectWithOriginAndSearchString.searchString
|
||||
}`,
|
||||
destination: `${redirectWithOriginAndSearchString.origin ?? ""}/d/${link}/${slug}${redirectWithOriginAndSearchString.searchString
|
||||
}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
|
||||
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
|
||||
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -30,6 +30,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
"email-verification"
|
||||
);
|
||||
const signupDisabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("disable-signup");
|
||||
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
|
||||
|
||||
const token = z.string().optional().parse(ctx.query.token);
|
||||
const redirectUrlData = z
|
||||
@@ -62,6 +63,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
isSAMLLoginEnabled,
|
||||
prepopulateFormValues: undefined,
|
||||
emailVerificationEnabled,
|
||||
onboardingV3Enabled,
|
||||
};
|
||||
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
getOrganizationSettings,
|
||||
@@ -9,7 +10,6 @@ import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { IS_CALCOM } from "@calcom/lib/constants";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
updateMeetingTokenIfExpired,
|
||||
} from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { CAL_VIDEO_MEETING_LINK_FOR_TESTING } from "@calcom/lib/constants";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
@@ -210,7 +210,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
exp: epochTimeFourteenDaysAfter,
|
||||
});
|
||||
|
||||
const sessionUserId = !!session?.user?.impersonatedBy ? session.user.impersonatedBy.id : session?.user.id;
|
||||
const sessionUserId = session?.user?.impersonatedBy ? session.user.impersonatedBy.id : session?.user.id;
|
||||
const isOrganizer = await checkIfUserIsHost({
|
||||
booking: {
|
||||
eventTypeId: bookingObj.eventType?.id,
|
||||
@@ -238,7 +238,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
videoReferencePassword,
|
||||
sessionUserId
|
||||
);
|
||||
if (!!meetingPassword) {
|
||||
if (meetingPassword) {
|
||||
bookingObj.references.forEach((bookRef) => {
|
||||
bookRef.meetingPassword = meetingPassword;
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { z } from "zod";
|
||||
|
||||
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import type { EventTypeAppSettingsComponentProps, EventTypeModel } from "@calcom/app-store/types";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
@@ -15,7 +16,6 @@ import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
||||
import { getAppOnboardingUrl } from "@calcom/lib/apps/getAppOnboardingUrl";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -177,7 +177,7 @@ const OnboardingPage = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventTypeGroups]);
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
|
||||
const updateMutation = trpc.viewer.eventTypesHeavy.update.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: data.eventType?.title }), "success");
|
||||
},
|
||||
@@ -309,21 +309,20 @@ const OnboardingPage = ({
|
||||
handleSetUpLater={handleSetUpLater}
|
||||
/>
|
||||
)}
|
||||
{currentStep === AppOnboardingSteps.CONFIGURE_STEP &&
|
||||
eventTypeGroups && (
|
||||
<ConfigureStepCard
|
||||
slug={appMetadata.slug}
|
||||
categories={appMetadata.categories}
|
||||
credentialId={credentialId}
|
||||
userName={userName}
|
||||
loading={updateMutation.isPending}
|
||||
formPortalRef={formPortalRef}
|
||||
setConfigureStep={setConfigureStep}
|
||||
eventTypeGroups={eventTypeGroups}
|
||||
handleSetUpLater={handleSetUpLater}
|
||||
isConferencing={isConferencing}
|
||||
/>
|
||||
)}
|
||||
{currentStep === AppOnboardingSteps.CONFIGURE_STEP && eventTypeGroups && (
|
||||
<ConfigureStepCard
|
||||
slug={appMetadata.slug}
|
||||
categories={appMetadata.categories}
|
||||
credentialId={credentialId}
|
||||
userName={userName}
|
||||
loading={updateMutation.isPending}
|
||||
formPortalRef={formPortalRef}
|
||||
setConfigureStep={setConfigureStep}
|
||||
eventTypeGroups={eventTypeGroups}
|
||||
handleSetUpLater={handleSetUpLater}
|
||||
isConferencing={isConferencing}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,13 +24,9 @@ import {
|
||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||
import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks";
|
||||
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 { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField, TITLE_FIELD } from "@calcom/lib/bookings/SystemField";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
@@ -771,7 +767,6 @@ export default function Success(props: PageProps) {
|
||||
<Fragment key={field.name}>
|
||||
<div
|
||||
className="text-emphasis mt-4 font-medium"
|
||||
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToSafeHTML(label),
|
||||
}}
|
||||
@@ -1084,7 +1079,8 @@ export default function Success(props: PageProps) {
|
||||
<span className="underline">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations" rel="noreferrer">
|
||||
href="https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations"
|
||||
rel="noreferrer">
|
||||
{t("resolve")}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
@@ -74,11 +74,9 @@ function useSystemSegments(userId?: number) {
|
||||
export default function Bookings(props: BookingsProps) {
|
||||
const pathname = usePathname();
|
||||
const systemSegments = useSystemSegments(props.userId);
|
||||
if (!pathname) return null;
|
||||
return (
|
||||
<DataTableProvider
|
||||
useSegments={useSegments}
|
||||
systemSegments={systemSegments}
|
||||
tableIdentifier={pathname || undefined}>
|
||||
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} systemSegments={systemSegments}>
|
||||
<BookingsContent {...props} />
|
||||
</DataTableProvider>
|
||||
);
|
||||
|
||||
@@ -271,7 +271,7 @@ export const InfiniteEventTypeList = ({
|
||||
},
|
||||
});
|
||||
|
||||
const setHiddenMutation = trpc.viewer.eventTypes.heavy.update.useMutation({
|
||||
const setHiddenMutation = trpc.viewer.eventTypesHeavy.update.useMutation({
|
||||
onMutate: async (data) => {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, useState, useReducer } from "react";
|
||||
|
||||
import {
|
||||
@@ -62,8 +63,10 @@ function reducer(state: CallDetailsState, action: CallDetailsAction): CallDetail
|
||||
}
|
||||
|
||||
function CallHistoryTable(props: CallHistoryProps) {
|
||||
const pathname = usePathname();
|
||||
if (!pathname) return null;
|
||||
return (
|
||||
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
|
||||
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={25}>
|
||||
<CallHistoryContent {...props} />
|
||||
</DataTableProvider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { DataTableProvider } from "@calcom/features/data-table/DataTableProvider";
|
||||
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||
import {
|
||||
@@ -13,9 +15,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
export default function InsightsRoutingFormResponsesPage() {
|
||||
const { t } = useLocale();
|
||||
const pathname = usePathname();
|
||||
|
||||
if (!pathname) return null;
|
||||
|
||||
return (
|
||||
<DataTableProvider useSegments={useSegments}>
|
||||
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments}>
|
||||
<InsightsOrgTeamsProvider>
|
||||
<div className="mb-4 space-y-4">
|
||||
<RoutingFormResponsesTable />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
import {
|
||||
@@ -39,8 +40,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
|
||||
export default function InsightsPage({ timeZone }: { timeZone: string }) {
|
||||
const pathname = usePathname();
|
||||
if (!pathname) return null;
|
||||
return (
|
||||
<DataTableProvider useSegments={useSegments} timeZone={timeZone}>
|
||||
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} timeZone={timeZone}>
|
||||
<InsightsOrgTeamsProvider>
|
||||
<InsightsPageContent />
|
||||
</InsightsOrgTeamsProvider>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useOnboardingStore } from "../store/onboarding-store";
|
||||
|
||||
export const OnboardingContinuationPrompt = () => {
|
||||
const router = useRouter();
|
||||
const { organizationDetails, resetOnboarding } = useOnboardingStore();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there's existing organization data
|
||||
const hasExistingData = organizationDetails.name && organizationDetails.link;
|
||||
setIsVisible(!!hasExistingData);
|
||||
}, [organizationDetails]);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
// Navigate to the next step in the flow (brand page)
|
||||
router.push("/onboarding/organization/brand");
|
||||
};
|
||||
|
||||
const handleStartOver = () => {
|
||||
resetOnboarding();
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in fixed bottom-4 right-4 z-50">
|
||||
<div className="bg-default border-muted shadow-default relative w-[360px] rounded-lg border p-4">
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="text-muted hover:text-emphasis absolute right-2 top-2 rounded-md p-1 transition-colors">
|
||||
<Icon name="x" className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="mb-3 pr-6">
|
||||
<h3 className="text-emphasis mb-1 text-base font-semibold">Continue onboarding?</h3>
|
||||
<p className="text-subtle text-sm">
|
||||
Would you like to carry on with onboarding for{" "}
|
||||
<span className="text-emphasis font-medium">{organizationDetails.name}</span> or start over?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button onClick={handleStartOver} color="secondary">
|
||||
Start over
|
||||
</Button>
|
||||
<Button onClick={handleContinue} color="primary">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Logo } from "@calcom/ui/components/logo";
|
||||
import { RadioAreaGroup } from "@calcom/ui/components/radio";
|
||||
|
||||
import { OnboardingContinuationPrompt } from "../components/onboarding-continuation-prompt";
|
||||
import { useOnboardingStore, type PlanType } from "../store/onboarding-store";
|
||||
|
||||
type OnboardingViewProps = {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
};
|
||||
|
||||
export const OnboardingView = ({ userName, userEmail }: OnboardingViewProps) => {
|
||||
const router = useRouter();
|
||||
const { selectedPlan, setSelectedPlan } = useOnboardingStore();
|
||||
|
||||
const handleContinue = () => {
|
||||
if (selectedPlan === "organization") {
|
||||
router.push("/onboarding/organization/details");
|
||||
}
|
||||
// TODO: Handle other plan types
|
||||
};
|
||||
|
||||
const allPlans = [
|
||||
{
|
||||
id: "personal" as PlanType,
|
||||
title: "For personal use",
|
||||
badge: "Free",
|
||||
description: "Good for individuals who are just starting out and simply want the essentials.",
|
||||
},
|
||||
{
|
||||
id: "team" as PlanType,
|
||||
title: "With my team",
|
||||
badge: "$15 p/mo p/person",
|
||||
description:
|
||||
"Highly recommended for small teams who seek to upgrade their time and perform better as a unit.",
|
||||
},
|
||||
{
|
||||
id: "organization" as PlanType,
|
||||
title: "For my organization",
|
||||
badge: "$37 p/mo p/person",
|
||||
description: "Robust scheduling for larger teams looking to have more control, privacy, and security.",
|
||||
},
|
||||
];
|
||||
|
||||
// Only show organization plan for company emails
|
||||
const plans = allPlans.filter((plan) => {
|
||||
if (plan.id === "organization") {
|
||||
return isCompanyEmail(userEmail);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip">
|
||||
<OnboardingContinuationPrompt />
|
||||
{/* Header */}
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<Logo className="h-5 w-auto" />
|
||||
|
||||
{/* Progress dots - centered */}
|
||||
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
|
||||
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
|
||||
<div className="bg-subtle h-1 w-1 rounded-full" />
|
||||
<div className="bg-subtle h-1 w-1 rounded-full" />
|
||||
<div className="bg-subtle h-1 w-1 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
|
||||
<p className="text-emphasis font-mediumu text-sm leading-none">{userEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex h-full w-full items-start justify-center px-6 py-8">
|
||||
<div className="flex w-full max-w-[600px] flex-col gap-6">
|
||||
{/* Card */}
|
||||
<div className="bg-muted border-muted relative rounded-xl border p-1">
|
||||
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
|
||||
{/* Card Header */}
|
||||
<div className="flex w-full gap-1.5 px-5 py-4">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<h1 className="font-cal text-xl font-semibold leading-6">Hey {userName}</h1>
|
||||
<p className="text-subtle text-sm font-medium leading-tight">
|
||||
To personalize your experience, what do you plan to use Cal.com for?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan options */}
|
||||
<RadioAreaGroup.Group
|
||||
value={selectedPlan ?? undefined}
|
||||
onValueChange={(value) => setSelectedPlan(value as PlanType)}
|
||||
className="flex w-full flex-col gap-1 rounded-[10px]">
|
||||
{plans.map((plan) => (
|
||||
<RadioAreaGroup.Item
|
||||
key={plan.id}
|
||||
value={plan.id}
|
||||
className={
|
||||
selectedPlan === plan.id ? "border-emphasis bg-default" : "bg-default border-subtle"
|
||||
}>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<p className="text-emphasis text-base font-semibold leading-4">{plan.title}</p>
|
||||
<Badge variant="gray" size="md" className="hidden h-4 rounded-md px-1 py-1 md:block">
|
||||
<span className="text-emphasis text-xs font-medium leading-3">{plan.badge}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Badge variant="gray" size="md" className="h-4 w-fit rounded-md px-1 py-1 md:hidden">
|
||||
<span className="text-emphasis text-xs font-medium leading-3">{plan.badge}</span>
|
||||
</Badge>
|
||||
<p className="text-subtle max-w-full text-sm font-normal leading-tight">
|
||||
{plan.description}
|
||||
</p>
|
||||
</div>
|
||||
</RadioAreaGroup.Item>
|
||||
))}
|
||||
</RadioAreaGroup.Group>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
|
||||
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import type { OnboardingState } from "../store/onboarding-store";
|
||||
|
||||
export const useSubmitOnboarding = () => {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const intentToCreateOrg = trpc.viewer.organizations.intentToCreateOrg.useMutation();
|
||||
|
||||
const submitOnboarding = async (store: OnboardingState, userEmail: string) => {
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const {
|
||||
selectedPlan,
|
||||
organizationDetails,
|
||||
organizationBrand,
|
||||
teams,
|
||||
invites,
|
||||
inviteRole,
|
||||
resetOnboarding,
|
||||
} = store;
|
||||
|
||||
if (selectedPlan !== "organization") {
|
||||
throw new Error("Only organization plan is currently supported");
|
||||
}
|
||||
|
||||
// Prepare teams data
|
||||
const teamsData = teams
|
||||
.filter((team) => team.name.trim().length > 0)
|
||||
.map((team) => ({
|
||||
id: -1, // New team
|
||||
name: team.name,
|
||||
isBeingMigrated: false,
|
||||
slug: null,
|
||||
}));
|
||||
|
||||
// Prepare invites data
|
||||
const invitedMembersData = invites
|
||||
.filter((invite) => invite.email.trim().length > 0)
|
||||
.map((invite) => ({
|
||||
email: invite.email,
|
||||
teamName: invite.team,
|
||||
teamId: -1,
|
||||
role: inviteRole,
|
||||
}));
|
||||
|
||||
const result = await intentToCreateOrg.mutateAsync({
|
||||
name: organizationDetails.name,
|
||||
slug: organizationDetails.link,
|
||||
bio: organizationDetails.bio || null,
|
||||
logo: organizationBrand.logo,
|
||||
brandColor: organizationBrand.color,
|
||||
bannerUrl: organizationBrand.banner,
|
||||
orgOwnerEmail: userEmail,
|
||||
seats: null,
|
||||
pricePerSeat: null,
|
||||
isPlatform: false,
|
||||
creationSource: CreationSource.WEBAPP,
|
||||
teams: teamsData,
|
||||
invitedMembers: invitedMembersData,
|
||||
});
|
||||
|
||||
// If there's a checkout URL, redirect to Stripe (billing enabled flow)
|
||||
if (result.checkoutUrl) {
|
||||
window.location.href = result.checkoutUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// No checkout URL means billing is disabled (self-hosted flow)
|
||||
// Organization has already been created by the backend
|
||||
showToast("Organization created successfully!", "success");
|
||||
// TODO: after this redirect we need to hard refresh the page to see org
|
||||
resetOnboarding();
|
||||
router.push("/getting-started");
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to create organization";
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage, "error");
|
||||
console.error("Organization creation error:", err);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
submitOnboarding,
|
||||
isSubmitting,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
"use client";
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Logo } from "@calcom/ui/components/logo";
|
||||
|
||||
import { useOnboardingStore } from "../../store/onboarding-store";
|
||||
|
||||
type OrganizationBrandViewProps = {
|
||||
userEmail: string;
|
||||
};
|
||||
|
||||
const BrandColorPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
t,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
t: (key: string) => string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="border-default bg-default flex h-7 w-32 items-center gap-2 rounded-lg border px-2 py-1.5">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
className="h-4 w-4 shrink-0 rounded-full border border-gray-200"
|
||||
style={{ backgroundColor: value }}
|
||||
aria-label={t("onboarding_pick_color_aria")}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content align="start" sideOffset={5}>
|
||||
<HexColorPicker color={value} onChange={onChange} className="!h-32 !w-32" />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
<input
|
||||
type="text"
|
||||
value={value.replace("#", "")}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value.startsWith("#") ? e.target.value : `#${e.target.value}`;
|
||||
onChange(newValue);
|
||||
}}
|
||||
className="text-emphasis grow border-none bg-transparent text-sm font-medium leading-4 outline-none"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const { organizationBrand, setOrganizationBrand, organizationDetails } = useOnboardingStore();
|
||||
|
||||
const [brandColor, setBrandColor] = useState("#000000");
|
||||
const [_logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [_bannerFile, setBannerFile] = useState<File | null>(null);
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
|
||||
|
||||
// Load from store on mount
|
||||
useEffect(() => {
|
||||
setBrandColor(organizationBrand.color);
|
||||
setLogoPreview(organizationBrand.logo);
|
||||
setBannerPreview(organizationBrand.banner);
|
||||
}, [organizationBrand]);
|
||||
|
||||
const handleLogoChange = (file: File | null) => {
|
||||
setLogoFile(file);
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result as string;
|
||||
setLogoPreview(base64);
|
||||
setOrganizationBrand({ logo: base64 });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
setLogoPreview(null);
|
||||
setOrganizationBrand({ logo: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBannerChange = (file: File | null) => {
|
||||
setBannerFile(file);
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result as string;
|
||||
setBannerPreview(base64);
|
||||
setOrganizationBrand({ banner: base64 });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
setBannerPreview(null);
|
||||
setOrganizationBrand({ banner: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
// Save brand color to store
|
||||
setOrganizationBrand({ color: brandColor });
|
||||
router.push("/onboarding/organization/teams");
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
// Skip brand setup and go to teams
|
||||
router.push("/onboarding/organization/teams");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-default flex min-h-screen w-full flex-col items-start overflow-clip rounded-xl">
|
||||
{/* Header */}
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<Logo className="h-5 w-auto" />
|
||||
|
||||
{/* Progress dots - centered */}
|
||||
<div className="absolute left-1/2 flex -translate-x-1/2 items-center justify-center gap-1">
|
||||
<div className="bg-emphasis h-1 w-1 rounded-full" />
|
||||
<div className="bg-emphasis h-1.5 w-1.5 rounded-full" />
|
||||
<div className="bg-subtle h-1 w-1 rounded-full" />
|
||||
<div className="bg-subtle h-1 w-1 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="bg-muted flex items-center gap-2 rounded-full px-3 py-2">
|
||||
<p className="text-emphasis text-sm font-medium leading-none">{userEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex h-full w-full items-start justify-center px-6 py-8">
|
||||
<div className="relative flex w-full max-w-[600px] flex-col gap-6">
|
||||
{/* Card */}
|
||||
<div className="bg-muted border-muted relative rounded-xl border p-1">
|
||||
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
|
||||
{/* Card Header */}
|
||||
<div className="flex w-full gap-1.5 px-5 py-4">
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<h1 className="font-cal text-xl font-semibold leading-6">{t("onboarding_org_brand_title")}</h1>
|
||||
<p className="text-subtle text-sm font-medium leading-tight">
|
||||
{t("onboarding_org_brand_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="bg-default border-muted w-full rounded-[10px] border">
|
||||
<div className="rounded-inherit flex w-full flex-col items-start overflow-clip">
|
||||
<div className="flex w-full flex-col items-start">
|
||||
<div className="flex w-full gap-6 px-5 py-4">
|
||||
{/* Left side - Form */}
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Brand Color */}
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<p className="text-emphasis text-sm font-medium leading-4">{t("brand_color")}</p>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<p className="text-subtle w-[98px] overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-4">
|
||||
{t("onboarding_primary_color_label")}
|
||||
</p>
|
||||
<BrandColorPicker
|
||||
value={brandColor}
|
||||
onChange={(value) => {
|
||||
setBrandColor(value);
|
||||
setOrganizationBrand({ color: value });
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo Upload */}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<p className="text-emphasis text-sm font-medium leading-4">{t("logo")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-muted border-muted relative h-16 w-16 shrink-0 overflow-hidden rounded-md border">
|
||||
{logoPreview && (
|
||||
<img
|
||||
src={logoPreview}
|
||||
alt={t("onboarding_logo_preview_alt")}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => document.getElementById("logo-upload")?.click()}>
|
||||
{t("upload")}
|
||||
</Button>
|
||||
<input
|
||||
id="logo-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => handleLogoChange(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-subtle text-xs font-normal leading-3">
|
||||
{t("onboarding_logo_size_hint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Banner Upload */}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<p className="text-emphasis text-sm font-medium leading-4">{t("onboarding_banner_label")}</p>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="bg-muted border-muted relative h-[92px] w-full overflow-hidden rounded-md border">
|
||||
{bannerPreview && (
|
||||
<img
|
||||
src={bannerPreview}
|
||||
alt={t("onboarding_banner_preview_alt")}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={() => document.getElementById("banner-upload")?.click()}>
|
||||
{t("upload")}
|
||||
</Button>
|
||||
<input
|
||||
id="banner-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => handleBannerChange(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-subtle text-xs font-normal leading-3">
|
||||
{t("onboarding_banner_size_hint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Preview */}
|
||||
<div className="bg-muted border-muted flex hidden h-[328px] w-full grow overflow-hidden rounded-[10px] border p-5 md:block">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<p className="text-subtle text-sm font-medium leading-4">{t("preview")}</p>
|
||||
<div className="border-subtle bg-default relative flex w-[110%] flex-col gap-2.5 rounded-md border px-5 pb-5 pt-[74px]">
|
||||
{/* Banner preview */}
|
||||
<div className="bg-muted border-muted absolute left-1 top-1 h-[92px] w-[272px] overflow-hidden rounded-[4px] border">
|
||||
{bannerPreview && (
|
||||
<img
|
||||
src={bannerPreview}
|
||||
alt={t("onboarding_banner_preview_alt")}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 p-1">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Logo preview */}
|
||||
<div className="bg-muted z-20 h-9 w-9 shrink-0 overflow-hidden rounded-md border-2 border-[var(--cal-bg)]">
|
||||
{logoPreview && (
|
||||
<img
|
||||
src={logoPreview}
|
||||
alt={t("onboarding_logo_preview_alt")}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-subtle text-sm font-medium capitalize leading-4 ">
|
||||
{organizationDetails.name || t("onboarding_preview_nameless")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="font-cal text-xl leading-5 tracking-[0.2px]">
|
||||
{t("onboarding_preview_example_title")}
|
||||
</p>
|
||||
<p className="text-subtle text-sm font-medium leading-5">
|
||||
{t("onboarding_preview_example_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[134, 104, 84, 104].map((width, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-1">
|
||||
<div className="bg-subtle h-5 w-5 shrink-0 rounded-full" />
|
||||
<div
|
||||
className="bg-subtle h-2.5 rounded-full"
|
||||
style={{ width: `${width}px` }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex w-full items-center justify-end gap-1 px-5 py-4">
|
||||
<Button color="minimal" className="rounded-[10px]" onClick={handleSkip}>
|
||||
{t("ill_do_this_later")}
|
||||
</Button>
|
||||
<Button color="primary" className="rounded-[10px]" onClick={handleContinue}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
"use server";
|
||||
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function checkSlugAvailability(slug: string): Promise<{
|
||||
available: boolean;
|
||||
message?: string;
|
||||
}> {
|
||||
if (!slug || slug.trim() === "") {
|
||||
return { available: false, message: "Slug is required" };
|
||||
}
|
||||
|
||||
// Check if slug is reserved
|
||||
if (RESERVED_SUBDOMAINS.includes(slug)) {
|
||||
return { available: false, message: "This slug is reserved" };
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
return { available: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
// Check if slug already exists
|
||||
const existingOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parentId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingOrg) {
|
||||
return { available: false, message: "This slug is already taken" };
|
||||
}
|
||||
|
||||
return { available: true };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user