Compare commits

..
29 Commits
Author SHA1 Message Date
Alex van Andel 4a914c2dfb chore: Release v4.5.3 (#16728)
release.yml / release (push) Canceled after 0s
2024-09-20 16:27:25 +01:00
Joe Au-Yeung 62d579d03d Default to connected Salesforce account as owner (#16747) 2024-09-20 15:27:05 +00:00
Morgan 74bed31edc chore: improve api-v2 rate limiting (#16746) 2024-09-20 15:15:48 +00:00
Alex van Andel 97f213f260 revert: be7d45afe2 (#16742) 2024-09-20 15:01:49 +00:00
Carina WollendorferandCarinaWolli b84cdc0d45 fix: bookings ilmits errror (#16739)
* use booker timezone

* add test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-09-20 13:35:12 +00:00
Syed Ali Shahbaz 8cb5a7080b feat: Add EXPAND accepting "team" in API V1 Bookings (#16736)
* add expand

* add expand to /[id]/_get

* add test

* fix test import

* --

* --

* fix test

* fix test

* revert unintentional commit
2024-09-20 11:46:26 +00:00
Lauris Skraucis 11f0d48d0b feat: onBeforeUpdate AvailabilitySettings prop (#16737) 2024-09-20 11:42:10 +00:00
Syed Ali Shahbaz c4a1382982 chore: add granular logging to the logger (#16734)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update turbo.json

* Update trpc-provider.tsx

* Update logger.ts

* update prisma index

* Update trpc.ts

* Update trpc-provider.tsx

* Update trpc.ts

* Update .env.example

* Update logger.ts
2024-09-20 09:06:37 +00:00
Calcom BotandReplexica e0922c5ee0 feat: update translations (#16726)
release.yml / release (push) Canceled after 0s
Co-authored-by: Replexica <support@replexica.com>
2024-09-19 16:58:47 +00:00
Benny JooandJoe Au-Yeung f8dc7a115a chore: App Router - remove references to /pages for remaining pages in /auth, /insights, /d, signup and add missing default /future page (#16589)
* remove references to /pages for sso, setup, signin pages in /auth

* remove references to pages for insights

* remove references to pages for d

* remove references to pages for signup

* add page for /future index page

* fix routing-forms

* fix

* add missing defaults

* use getServerSessionForAppDir instead

* fix apps/[slug]/[...pages]

* fix metadata in apps/slug/pages

* refactor

* refactor

* remove duplicate code for PageProps

* remove references to pages for /reschedule

* fix

* fix routing forms

* type fix

* fix routing forms again

* revert changes for app/slug/pages

* revert

* revert changes in yarn lock

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-09-19 15:06:51 +00:00
sean-brydonandPeer Richelsen a60aa1bf6c add create license key to admin tab if IS_CALCOM (#16721)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-09-19 13:09:14 +00:00
Hariom Balhara bc203d7439 Release latest embed (#16713) 2024-09-19 11:02:17 +00:00
Udit Takkar d82847035f fix: type error on booking cancelled page (#16716) 2024-09-19 10:13:16 +00:00
Hariom Balhara 6d133c26f8 Narrow argument type to the specific event being listened to (#16714) 2024-09-19 09:42:32 +00:00
Benny Joo 3ae53a4b6e chore: App router - Fix errors in settings pages & add remaining pages (#16648)
* fix settings/admin/page

* remove references to pages for settings/teams

* remove double layouts

* move pages that do not need layout out of (settings) group

* fix

* fix

* fix

* fix

* add sso page and fix layouts
2024-09-19 09:59:32 +01:00
Lauris Skraucis 63339d8c01 fix: connect apple calendar atom nested <button>'s (#16715) 2024-09-19 08:49:21 +00:00
Lauris SkraucisandMorgan 44410c87f0 docs: v2 schedules and event-types swagger (#16608)
* docs: schedules

* docs: wip event-types

* docs:event-types

* docs:event-types

* Delete yarn.lock

* Revert "Delete yarn.lock"

This reverts commit bb3eb23508c0fa2e216ba7bcb97756215f889daa.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-19 10:00:17 +03:00
Omar López c016241004 chore: removes unnecessary prisma env warnings (#16708)
* chore: removes unnecessary prisma env warnings

* Revert lockfile

Signed-off-by: zomars <zomars@me.com>

* Update yarn lock

Signed-off-by: zomars <zomars@me.com>

---------

Signed-off-by: zomars <zomars@me.com>
2024-09-19 07:32:23 +05:30
Benny Joo 019ef6de2a chore: App router migration - (/bookings, /booking) Move getServerSideProps/getStaticProps to /lib (#16458)
* booking: Move getServerSideProps to /lib

* remove duplicate

* bookings: Move getStaticProps to /lib

* fix type check

* replace setHeader with a server action call in withEmbedSSR

* Add env variables for booking
2024-09-18 19:09:57 -04:00
Omar López a2b064b269 feat: per user and team feature flags (#16040) 2024-09-18 21:27:47 +00:00
abb4fd307f refactor: event setup tab web wrapper (#16672)
* chore: EventTypeWebWrapper base pr

* fixup! chore: EventTypeWebWrapper base pr

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* wip

* fix: error

* Event type atom wrapper handles app dir

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* fixup! fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* remove console log

* refactor: event-type-atom (setup tab)

* review comments

* update: better naming

* fixup! review comments

* revert: defaultMenuIsOpen --will create a separate PR

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-09-18 20:14:23 +03:00
Peer Richelsen a9ebb0d160 feat: Retell AI (#16706)
* added retell ai to app store

* added retell ai
2024-09-18 17:08:01 +00:00
sean-brydon 1058c0e171 fix(eslint): Adds eslint-ignore-patterns to eslint config (#16701) 2024-09-18 15:17:52 +00:00
Anik Dhabal Babu 021e5f3833 fix: fixed more flaky e2e tests (round 2) (#16660)
* fix: fixed more flaky e2e tests (round 2)

* update

* fix and update

* update
2024-09-18 23:25:44 +09:00
Lauris Skraucis 5b5a42e72d refactor: v2 and atoms booking fields (#16685)
* refactor: define inputs and outputs by separating default and custom fields

* refactor: event type api <-> internal transformers

* refactor: v2 use refactored libraries to transform input and output booking fields

* refactor: dont allow custom slugs with default reserved slugs

* refactor: input service store only specific default system fields

* refactor: atoms display only specific default system fields

* refactor: delete unused system field constants

* fix: tests

* fix: old event types

* fix: imports

* fix: import

* chore: release libraries and use v2 with them

* fix: unit tests

* refactor: push email in case of missing phone

* refactor: satisfies check for event-type locations
2024-09-18 13:59:31 +02:00
MorganandSomay Chauhan 2b43d9a716 chore: EventTypeWebWrapper base pr (#16550)
* chore: EventTypeWebWrapper base pr

* fixup! chore: EventTypeWebWrapper base pr

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* wip

* fix: error

* Event type atom wrapper handles app dir

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* fixup! fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

* remove console log

* review comments

* fixup! review comments

* fixup! Merge branch 'main' into morgan/cal-4183-eventtypeatom-web-wrapper-base

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2024-09-18 12:26:04 +03:00
Omar López 3a5325f2a8 fix: new user detection for dub analytics (#16695) 2024-09-17 19:19:25 -07:00
Calcom BotandReplexica 5ad73607f0 feat: update translations (#16693)
Co-authored-by: Replexica <support@replexica.com>
2024-09-17 23:21:10 +00:00
Carina Wollendorfer 5067f0dd16 fix: dates in ooo booking redirect email (#16692) 2024-09-17 22:28:40 +00:00
289 changed files with 14307 additions and 6859 deletions
+6
View File
@@ -343,12 +343,14 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
APP_ROUTER_BOOKING_ENABLED=0
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
APP_ROUTER_WORKFLOWS_ENABLED=0
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
APP_ROUTER_AVAILABILITY_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
@@ -392,3 +394,7 @@ VAPID_PRIVATE_KEY=
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
NEXT_PUBLIC_WEBSITE_TERMS_URL=
# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
File diff suppressed because one or more lines are too long
+20 -2
View File
@@ -173,11 +173,29 @@ yarn dx
#### Development tip
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
```
for Logger level to be set at info, for example.
#### Gitpod Setup
+25 -1
View File
@@ -1,6 +1,13 @@
import { z } from "zod";
import { _AttendeeModel, _BookingModel as Booking, _PaymentModel, _UserModel } from "@calcom/prisma/zod";
import {
_AttendeeModel,
_BookingModel as Booking,
_EventTypeModel,
_PaymentModel,
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { schemaQueryUserId } from "./shared/queryUserId";
@@ -46,7 +53,23 @@ export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
.merge(schemaBookingEditParams)
.omit({ uid: true });
const teamSchema = _TeamModel.pick({
name: true,
slug: true,
});
export const schemaBookingReadPublic = Booking.extend({
eventType: _EventTypeModel
.pick({
title: true,
slug: true,
})
.merge(
z.object({
team: teamSchema.nullish(),
})
)
.nullish(),
attendees: z
.array(
_AttendeeModel.pick({
@@ -87,6 +110,7 @@ export const schemaBookingReadPublic = Booking.extend({
timeZone: true,
attendees: true,
user: true,
eventType: true,
payment: true,
metadata: true,
status: true,
@@ -0,0 +1,12 @@
import { z } from "zod";
const expandEnum = z.enum(["team"]);
export const schemaQuerySingleOrMultipleExpand = z
.union([
expandEnum, // Allow a single value from the enum
z.array(expandEnum).refine((arr) => new Set(arr).size === arr.length, {
message: "Array values must be unique",
}), // Allow an array of enum values, with uniqueness constraint
])
.optional();
+14 -1
View File
@@ -4,6 +4,7 @@ import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
/**
@@ -89,9 +90,21 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];
const booking = await prisma.booking.findUnique({
where: { id },
include: { attendees: true, user: true, payment: true },
include: {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
},
});
return { booking: schemaBookingReadPublic.parse(booking) };
}
+9
View File
@@ -12,6 +12,7 @@ import {
} from "~/lib/utils/retrieveScopedAccessibleUsers";
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
/**
@@ -216,10 +217,18 @@ export async function handler(req: NextApiRequest) {
args.take = take;
args.skip = skip;
}
const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];
args.include = {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
};
const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
@@ -164,7 +164,6 @@ describe("GET /api/bookings", async () => {
const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
console.log(booking);
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime());
});
});
@@ -188,4 +187,27 @@ describe("GET /api/bookings", async () => {
});
});
});
describe("Expand feature to add relational data in return payload", () => {
it("Returns only team data when expand=team is set", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
expand: "team",
},
pagination: DefaultPagination,
});
req.userId = adminUser.id;
req.isOrganizationOwnerOrAdmin = true;
const responseData = await handler(req);
console.log("bookings=>", responseData.bookings);
responseData.bookings.forEach((booking) => {
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
});
});
});
});
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.34",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+4 -2
View File
@@ -1,4 +1,5 @@
import appConfig from "@/config/app";
import { CustomThrottlerGuard } from "@/lib/throttler-guard";
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
@@ -15,7 +16,7 @@ import { BullModule } from "@nestjs/bull";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { seconds, ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { seconds, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
import { AppController } from "./app.controller";
@@ -32,6 +33,7 @@ import { AppController } from "./app.controller";
BullModule.forRoot({
redis: `${process.env.REDIS_URL}${process.env.NODE_ENV === "production" ? "?tls=true" : ""}`,
}),
// Rate limiting here is handled by the CustomThrottlerGuard
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
@@ -59,7 +61,7 @@ import { AppController } from "./app.controller";
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
useClass: CustomThrottlerGuard,
},
],
})
@@ -36,7 +36,7 @@ import {
InternalServerErrorException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
@@ -47,7 +47,7 @@ import { PrismaClient } from "@calcom/prisma";
version: [VERSION_2024_04_15, VERSION_2024_06_11],
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
@DocsExcludeController(true)
export class EventTypesController_2024_04_15 {
constructor(
private readonly eventTypesService: EventTypesService_2024_04_15,
@@ -15,6 +15,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
import { EventType } from "@calcom/prisma/client";
@@ -127,8 +128,17 @@ export class EventTypesService_2024_04_15 {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}
await updateEventType({
input: { id: eventTypeId, ...body },
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -250,7 +250,6 @@ describe("Event types Endpoints", () => {
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.locations).toEqual(body.locations);
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
expect(createdEventType.ownerId).toEqual(user.id);
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
@@ -259,6 +258,16 @@ describe("Event types Endpoints", () => {
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
expect(createdEventType.recurrence).toEqual(body.recurrence);
const responseBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
eventType = responseBody.data;
});
});
@@ -476,6 +485,16 @@ describe("Event types Endpoints", () => {
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, type: "radioInput", slug: "location", required: false },
{ isDefault: true, required: true, slug: "title", type: "text" },
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
];
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -545,7 +564,7 @@ describe("Event types Endpoints", () => {
.expect(400);
});
it("should return empty bookingFields if system fields are the only one in database", async () => {
it("should return system bookingFields stored in database", async () => {
const legacyEventTypeInput = {
title: "legacy event type",
description: "legacy event type description",
@@ -638,11 +657,11 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([]);
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
});
});
it("should return user created bookingFields among system fields in the database", async () => {
it("should return user created bookingFields with system fields", async () => {
const userDefinedBookingField = {
name: "team",
type: "textarea",
@@ -755,7 +774,9 @@ describe("Event types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
@@ -24,7 +24,7 @@ import {
Delete,
Query,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
@@ -39,12 +39,23 @@ import {
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-14\``,
required: true,
})
export class EventTypesController_2024_06_14 {
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async createEventType(
@Body() body: CreateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
@@ -60,6 +71,12 @@ export class EventTypesController_2024_06_14 {
@Get("/:eventTypeId")
@Permissions([EVENT_TYPE_READ])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async getEventTypeById(
@Param("eventTypeId") eventTypeId: string,
@GetUser() user: UserWithProfile
@@ -91,6 +108,12 @@ export class EventTypesController_2024_06_14 {
@Patch("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param("eventTypeId") eventTypeId: number,
@@ -108,6 +131,12 @@ export class EventTypesController_2024_06_14 {
@Delete("/:eventTypeId")
@Permissions([EVENT_TYPE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@GetUser("id") userId: number
@@ -6,8 +6,8 @@ import { Injectable } from "@nestjs/common";
import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
@@ -30,8 +30,8 @@ type InputEventTransformed = Omit<
> & {
length: number;
slug: string;
locations?: ReturnType<typeof transformApiEventTypeLocations>;
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
locations?: ReturnType<typeof transformLocationsApiToInternal>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
};
@Injectable()
@@ -1,12 +1,16 @@
import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
transformApiEventTypeRecurrence,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemAfterFieldRescheduleReason,
} from "@calcom/platform-libraries";
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -30,11 +34,12 @@ export class InputEventTypesService_2024_06_14 {
...rest
} = inputEventType;
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -59,11 +64,15 @@ export class InputEventTypesService_2024_06_14 {
...rest
} = inputEventType;
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
schedule: scheduleId,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
@@ -77,23 +86,35 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
return transformApiEventTypeLocations(inputLocations);
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
return transformApiEventTypeBookingFields(inputBookingFields);
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
if (hasMultipleLocations) {
defaultFieldsBefore.push(systemBeforeFieldLocation);
}
const customFields = transformBookingFieldsApiToInternal(inputBookingFields);
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
return transformApiEventTypeIntervalLimits(inputBookingFields);
return transformIntervalLimitsApiToInternal(inputBookingFields);
}
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
return !!res ? res : {};
}
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
return transformApiEventTypeRecurrence(recurrence);
return transformRecurrenceApiToInternal(recurrence);
}
}
@@ -4,17 +4,17 @@ import type { EventType, User, Schedule } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
SystemField,
UserField,
CustomField,
parseBookingLimit,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
getResponseEventTypeRecurrence,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
} from "@calcom/platform-libraries";
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
@@ -144,19 +144,20 @@ export class OutputEventTypesService_2024_06_14 {
transformLocations(locations: any) {
if (!locations) return [];
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
}
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
if (!inputBookingFields) return [];
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
return getResponseEventTypeBookingFields(userFields);
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
if (!bookingFields) return [];
return transformBookingFieldsInternalToApi(bookingFields);
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
const recurringEventParsed = parseRecurringEvent(recurringEvent);
return getResponseEventTypeRecurrence(recurringEventParsed);
if (!recurringEventParsed) return null;
return transformRecurrenceInternalToApi(recurringEventParsed);
}
transformMetadata(metadata: any) {
@@ -182,10 +183,10 @@ export class OutputEventTypesService_2024_06_14 {
transformIntervalLimits(bookingLimits: any) {
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
}
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
return getResponseEventTypeFutureBookingLimits(bookingLimits);
return transformFutureBookingLimitsInternalToApi(bookingLimits);
}
}
@@ -23,7 +23,7 @@ import {
Patch,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiResponse, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -36,7 +36,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input"
version: VERSION_2024_04_15_VALUE,
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Schedules")
@DocsExcludeController(true)
export class SchedulesController_2024_04_15 {
constructor(private readonly schedulesService: SchedulesService_2024_04_15) {}
@@ -17,7 +17,7 @@ import {
Patch,
UseGuards,
} from "@nestjs/common";
import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiHeader, ApiOperation, ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -38,11 +38,38 @@ import {
})
@UseGuards(ApiAuthGuard, PermissionsGuard)
@DocsTags("Schedules")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-11\``,
required: true,
})
@ApiHeader({
name: "Authorization",
description:
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
required: true,
})
export class SchedulesController_2024_06_11 {
constructor(private readonly schedulesService: SchedulesService_2024_06_11) {}
@Post("/")
@Permissions([SCHEDULE_WRITE])
@ApiOperation({
summary: "Create a schedule",
description: `
The point of creating schedules is for event types to be available at specific times.
First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule.
1. If you passed \`timeZone\` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via \`AvailabilitySettings\` atom.
2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying
\`"isDefault": true\` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom.
Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule.
After creating a non default schedule you can update event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint.
When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc.
`,
})
async createSchedule(
@GetUser() user: UserWithProfile,
@Body() bodySchedule: CreateScheduleInput_2024_06_11
@@ -59,7 +86,7 @@ export class SchedulesController_2024_06_11 {
@Permissions([SCHEDULE_READ])
@ApiResponse({
status: 200,
description: "Returns the default schedule",
description: "Returns the default schedule of the authenticated user",
type: GetDefaultScheduleOutput_2024_06_11,
})
async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise<GetScheduleOutput_2024_06_11> {
@@ -88,6 +115,9 @@ export class SchedulesController_2024_06_11 {
@Get("/")
@Permissions([SCHEDULE_READ])
@ApiOperation({
description: "Returns all schedules of the authenticated user",
})
async getSchedules(@GetUser() user: UserWithProfile): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(user.id);
+45
View File
@@ -0,0 +1,45 @@
import { isApiKey } from "@/lib/api-key";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { ThrottlerGuard, ThrottlerModuleOptions, ThrottlerStorage } from "@nestjs/throttler";
import { Request } from "express";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
private logger = new Logger("CustomThrottlerGuard");
constructor(
options: ThrottlerModuleOptions,
storageService: ThrottlerStorage,
reflector: Reflector,
private readonly config: ConfigService
) {
super(options, storageService, reflector);
}
protected async getTracker(request: Request): Promise<string> {
const authorizationHeader = request.get("Authorization")?.replace("Bearer ", "");
if (authorizationHeader) {
return isApiKey(authorizationHeader, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
? `api_key_${authorizationHeader}`
: `access_token_${authorizationHeader}`;
}
const oauthClientId = request.get(X_CAL_CLIENT_ID);
if (oauthClientId) {
return oauthClientId;
}
if (request.ip) {
return request.ip;
}
this.logger.log(`no tracker found: ${request.url}`);
return "unknown";
}
}
@@ -79,7 +79,6 @@ export class OAuthClientUsersController {
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
);
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
console.log("asap createUser client", JSON.stringify(client, null, 2));
const isPlatformManaged = true;
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
@@ -29,7 +29,11 @@ export class CreateManagedUserInput {
@IsTimeZone()
@IsOptional()
@CapitalizeTimeZone()
@ApiProperty({ example: "America/New_York" })
@ApiProperty({
example: "America/New_York",
description: `Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.`,
})
timeZone?: string;
@IsEnum(Locales)
File diff suppressed because it is too large Load Diff
+3
View File
@@ -22,4 +22,7 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
process.env = {
...env,
...process.env,
NEXT_PUBLIC_VAPID_PUBLIC_KEY:
"BIds0AQJ96xGBjTSMHTOqLBLutQE7Lu32KKdgSdy7A2cS4mKI2cgb3iGkhDJa5Siy-stezyuPm8qpbhmNxdNHMw",
VAPID_PRIVATE_KEY: "6cJtkASCar5sZWguIAW7OjvyixpBw9p8zL8WDDwk9Jk",
};
+9
View File
@@ -1,5 +1,14 @@
# @calcom/web
## 4.5.2
### Patch Changes
- Updated dependencies
- @calcom/embed-core@1.5.1
- @calcom/embed-react@1.5.1
- @calcom/embed-snippet@1.3.1
## 4.0.8
### Patch Changes
+2
View File
@@ -28,7 +28,9 @@ const ROUTES: [URLPattern, boolean][] = [
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
["/booking/:path*", process.env.APP_ROUTER_BOOKING_ENABLED === "1"] as const,
["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const,
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const,
["/more", process.env.APP_ROUTER_MORE_ENABLED === "1"] as const,
["/maintenance", process.env.APP_ROUTER_MAINTENANCE_ENABLED === "1"] as const,
+1 -1
View File
@@ -55,7 +55,7 @@ export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
(typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === 'number' && process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) || (opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
@@ -1,9 +1,8 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { SearchParams } from "app/_types";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsResult } from "next";
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import z from "zod";
@@ -18,13 +17,7 @@ const paramsSchema = z.object({
pages: z.array(z.string()),
});
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
@@ -34,7 +27,7 @@ export const generateMetadata = async ({
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
const data = await getData(legacyContext);
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
const formName = form?.name ?? "Forms";
const formName = form?.name ?? "Routing Forms";
const formDescription = form?.description ?? "";
return await _generateMetadata(
+4 -3
View File
@@ -1,12 +1,13 @@
import Setup from "@pages/auth/setup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
import Setup from "~/auth/setup-view";
import type { PageProps } from "~/auth/setup-view";
export default WithLayout({
getLayout: null,
Page: Setup,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
getData: withAppDirSsr<PageProps>(getServerSideProps),
})<"P">;
+5 -4
View File
@@ -1,13 +1,14 @@
import signin from "@pages/auth/signin";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
import SignIn from "~/auth/signin-view";
import type { PageProps } from "~/auth/signin-view";
export default WithLayout({
getLayout: null,
Page: signin,
Page: SignIn,
// @ts-expect-error TODO: fix this
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
getData: withAppDirSsr<PageProps>(getServerSideProps),
})<"P">;
@@ -1,12 +1,13 @@
import Provider from "@pages/auth/sso/[provider]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
import type { SSOProviderPageProps } from "~/auth/sso/provider-view";
import SSOProviderView from "~/auth/sso/provider-view";
export default WithLayout({
getLayout: null,
Page: Provider,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
Page: SSOProviderView,
getData: withAppDirSsr<SSOProviderPageProps>(getServerSideProps),
})<"P">;
+5 -3
View File
@@ -1,11 +1,13 @@
import DirectSSOLogin from "@pages/auth/sso/direct";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
import type { SSODirectPageProps } from "~/auth/sso/direct-view";
import SSODirectView from "~/auth/sso/direct-view";
export default WithLayout({
getLayout: null,
Page: DirectSSOLogin,
getData: withAppDirSsr(getServerSideProps),
Page: SSODirectView,
getData: withAppDirSsr<SSODirectPageProps>(getServerSideProps),
})<"P">;
+2 -8
View File
@@ -1,5 +1,5 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -11,13 +11,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import OldPage from "~/bookings/views/bookings-single-view";
import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const { bookingInfo, eventType, recurringBookings } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams)
);
@@ -3,8 +3,6 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { InferGetStaticPropsType } from "next";
import { APP_NAME } from "@calcom/lib/constants";
import { validStatuses } from "~/bookings/lib/validStatuses";
import Page from "~/bookings/views/bookings-listing-view";
import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
@@ -14,8 +12,8 @@ const getData = withAppDirSsg<Y>(getStaticProps);
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${APP_NAME} | ${t("bookings")}`,
() => ""
() => "Bookings",
() => "Create events to share for people to book on your calendar."
);
export const generateStaticParams = async () => {
+7 -11
View File
@@ -1,6 +1,5 @@
import LegacyPage from "@pages/d/[link]/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -10,14 +9,11 @@ import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
import Type from "~/d/[link]/d-type-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const pageProps = await getData(legacyCtx);
@@ -42,5 +38,5 @@ export const generateMetadata = async ({
);
};
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, Page: Type, getData })<"P">;
@@ -4,12 +4,12 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { EventType } from "@calcom/atoms/monorepo";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps";
import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { eventType } = await getData(legacyCtx);
@@ -21,5 +21,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
};
const getData = withAppDirSsr(getServerSideProps);
const Page = (props: EventTypePageProps) => <EventTypePageWrapper {...props} isAppDir={true} />;
const Page = ({ type, ...rest }: EventTypePageProps) => <EventType {...rest} id={type} isAppDir={true} />;
export default WithLayout({ getLayout: null, getData, Page })<"P">;
+5 -4
View File
@@ -1,10 +1,11 @@
import LegacyPage from "@pages/insights/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/insights/getServerSideProps";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
import type { PageProps } from "~/insights/insights-view";
import InsightsPage from "~/insights/insights-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -12,6 +13,6 @@ export const generateMetadata = async () =>
(t) => t("insights_subtitle")
);
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
export default WithLayout({ getLayout: null, getData, Page: InsightsPage });
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -13,13 +13,7 @@ import TeamPage from "~/team/team-view";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const teamName = props.team.name || "Nameless Team";
+2 -8
View File
@@ -1,5 +1,5 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const teamName = props.team.name || "Nameless Team";
@@ -1,5 +1,5 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -13,13 +13,7 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { PageProps } from "~/team/type-view";
import TypePage from "~/team/type-view";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { user: username, slug: eventSlug, booking } = props;
@@ -1,5 +1,5 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const teamName = props.team.name || "Nameless Team";
+13
View File
@@ -0,0 +1,13 @@
import { redirect } from "next/navigation";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
const RedirectPage = async () => {
const session = await getServerSessionForAppDir();
if (!session?.user?.id) {
redirect("/auth/login");
}
redirect("/event-types");
};
export default RedirectPage;
@@ -1,14 +1,15 @@
import { getServerSideProps } from "@pages/reschedule/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]";
import type { PageProps } from "app/_types";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import withEmbedSsr from "@lib/withEmbedSsr";
const getData = withEmbedSsr(_getServerSideProps);
const Page = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx);
await getData(legacyCtx);
return null;
};
@@ -1,10 +1,10 @@
import { getServerSideProps } from "@pages/reschedule/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { headers, cookies } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/reschedule/[uid]/getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -1,12 +1,19 @@
import type { PageProps } from "app/_types";
import { redirect } from "next/navigation";
import z from "zod";
const getPageProps = () => {
return redirect(`/apps/routing-forms/forms`);
};
const Page = () => {
getPageProps();
const paramsSchema = z
.object({
pages: z.array(z.string()),
})
.catch({
pages: [],
});
return null;
const Page = ({ params, searchParams }: PageProps) => {
const { pages } = paramsSchema.parse({ ...params, ...searchParams });
redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
};
export default Page;
+2 -25
View File
@@ -1,30 +1,7 @@
import type { PageProps } from "app/_types";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import z from "zod";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
const paramsSchema = z
.object({
pages: z.array(z.string()),
})
.catch({
pages: [],
});
const getPageProps = async (context: GetServerSidePropsContext) => {
const { pages } = paramsSchema.parse(context.params);
return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
};
const Page = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
await getPageProps(legacyCtx);
return null;
const Page = () => {
redirect("/apps/routing-forms/forms");
};
export default Page;
@@ -1,8 +1,4 @@
import LegacyPage from "@pages/settings/admin/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -10,4 +6,5 @@ export const generateMetadata = async () =>
() => "admin_description"
);
export default WithLayout({ getServerLayout: getLayout, Page: LegacyPage })<"P">;
const Page = () => <h1>Admin index</h1>;
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,11 +1,5 @@
import { _generateMetadata } from "app/_utils";
import BillingPage, { generateMetadata } from "../../billing/page";
import Page from "~/settings/billing/billing-view";
export { generateMetadata };
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("manage_billing_description")
);
export default Page;
export default BillingPage;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/general";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,15 @@ export const generateMetadata = async () =>
(t) => t("general_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/members";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/members";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,15 @@ export const generateMetadata = async () =>
(t) => t("organization_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("organization_members")} description={t("organization_description")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/profile";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,18 @@ export const generateMetadata = async () =>
(t) => t("profile_org_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader
title={t("profile")}
description={t("profile_org_description")}
borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -0,0 +1,24 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sso_configuration"),
(t) => t("sso_configuration_description_orgs")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("profile")} description={t("profile_org_description")}>
<OrgSSOView isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,18 @@ export const generateMetadata = async () =>
(t) => t("appearance_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader
title={t("booking_appearance")}
description={t("appearance_team_description")}
borderInShellHeader={false}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,16 @@ export const generateMetadata = async () =>
(t) => t("members_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
// TODO: Add CTA Button
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,15 @@ export const generateMetadata = async () =>
(t) => t("profile_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("profile")} description={t("profile_team_description")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
export default WithLayout({ getServerLayout: getLayout });
@@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("org_admin_other_teams"),
(t) => t("org_admin_other_teams_description")
);
export default Page;
@@ -0,0 +1,24 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("org_admin_other_teams"),
(t) => t("org_admin_other_teams_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("org_admin_other_teams")} description={t("org_admin_other_teams_description")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,18 @@ export const generateMetadata = async () =>
(t) => t("appearance_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader
title={t("booking_appearance")}
description={t("appearance_team_description")}
borderInShellHeader={false}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,11 +1,4 @@
import { _generateMetadata } from "app/_utils";
import BillingPage, { generateMetadata } from "../../../billing/page";
import Page from "~/settings/billing/billing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("team_billing_description")
);
export default Page;
export { generateMetadata };
export default BillingPage;
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-members-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/teams/pages/team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,15 @@ export const generateMetadata = async () =>
(t) => t("members_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { _generateMetadata, getFixedT } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-profile-view";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/teams/pages/team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,18 @@ export const generateMetadata = async () =>
(t) => t("profile_team_description")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader
title={t("profile")}
description={t("profile_team_description")}
borderInShellHeader={true}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
export default Page;
@@ -0,0 +1,24 @@
import { _generateMetadata, getFixedT } from "app/_utils";
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
import LegacyPage from "@calcom/features/ee/teams/pages/team-listing-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("teams"),
(t) => t("create_manage_teams_collaborative")
);
const Page = async () => {
const session = await getServerSessionForAppDir();
const t = await getFixedT(session?.user.locale || "en");
return (
<SettingsHeader title={t("teams")} description={t("create_manage_teams_collaborative")}>
<LegacyPage isAppDir={true} />
</SettingsHeader>
);
};
export default Page;
@@ -1,6 +1,9 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/about-view";
export const generateMetadata = async () =>
@@ -9,7 +12,10 @@ export const generateMetadata = async () =>
(t) => t("about_your_organization_description")
);
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getLayout: LayoutWrapper,
getData,
});
@@ -1,6 +1,9 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/add-teams-view";
export const generateMetadata = async () =>
@@ -9,7 +12,10 @@ export const generateMetadata = async () =>
(t) => t("create_your_teams_description")
);
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getLayout: LayoutWrapper,
getData,
});
@@ -1,6 +1,9 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/onboard-members-view";
export const generateMetadata = async () =>
@@ -9,7 +12,10 @@ export const generateMetadata = async () =>
(t) => t("invite_organization_admins_description")
);
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getLayout: LayoutWrapper,
getData,
});
@@ -1,4 +1,5 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import Page from "@calcom/features/ee/organizations/pages/settings/appearance";
@@ -8,4 +9,4 @@ export const generateMetadata = async () =>
(t) => t("appearance_org_description")
);
export default Page;
export default WithLayout({ Page });
@@ -1,11 +1,12 @@
import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/event-type";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import CreateTeamEventType, { GetLayout } from "~/settings/teams/[id]/event-types-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("add_new_team_event_type"),
(t) => t("new_event_type_to_book_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;
export default WithLayout({ Page: CreateTeamEventType, getLayout: GetLayout })<"P">;
@@ -1,11 +1,12 @@
import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/onboard-members";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import AddNewTeamMembers, { GetLayout } from "~/settings/teams/[id]/onboard-members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("add_team_members"),
(t) => t("add_team_members_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;
export default WithLayout({ Page: AddNewTeamMembers, getLayout: GetLayout })<"P">;
@@ -1,11 +1,12 @@
import LegacyPage, { LayoutWrapper } from "@pages/settings/teams/new/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import CreateNewTeamView, { LayoutWrapper } from "~/settings/teams/new/create-new-team-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("create_new_team"),
(t) => t("create_new_team_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper })<"P">;
export default WithLayout({ Page: CreateNewTeamView, getLayout: LayoutWrapper })<"P">;
+4 -2
View File
@@ -1,10 +1,12 @@
import LegacyPage, { type SignupProps } from "@pages/signup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/signup/getServerSideProps";
import type { SignupProps } from "~/signup-view";
import Signup from "~/signup-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sign_up"),
@@ -14,7 +16,7 @@ export const generateMetadata = async () =>
const getData = withAppDirSsr<SignupProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
Page: Signup,
getLayout: null,
getData,
})<"P">;
+1
View File
@@ -51,6 +51,7 @@ export function WithLayout<T extends Record<string, any>>({
requiresLicense={requiresLicense || !!(Page && "requiresLicense" in Page && Page.requiresLicense)}
nonce={nonce}
themeBasis={null}
isThemeSupported={!!(Page && "isThemeSupported" in Page && Page.isThemeSupported)}
isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)}
{...props}>
{pageWithServerLayout}
+1 -1
View File
@@ -177,7 +177,7 @@ export const AppPage = ({
typeof descriptionItem === "object" ? (
<div
key={`iframe-${index}`}
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center overflow-hidden rounded-md last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
<iframe allowFullScreen {...descriptionItem.iframe} />
</div>
) : (
@@ -323,7 +323,7 @@ function BookingListItem(booking: BookingItemProps) {
const urlSearchParams = new URLSearchParams({
allRemainingBookings: isTabRecurring.toString(),
});
if (booking.attendees[0].email) urlSearchParams.set("email", booking.attendees[0].email);
if (booking.attendees?.[0]?.email) urlSearchParams.set("email", booking.attendees[0].email);
return `/booking/${booking.uid}?${urlSearchParams.toString()}`;
};
@@ -13,7 +13,7 @@ import type { AppProps } from "@lib/app-providers";
import { ssrInit } from "@server/lib/ssr";
type AppPageType = {
getServerSideProps: AppGetServerSideProps;
getServerSideProps?: AppGetServerSideProps;
// A component than can accept any properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ((props: any) => JSX.Element) &
@@ -0,0 +1,180 @@
// page can be a server component
import type { GetServerSidePropsContext } from "next";
import { URLSearchParams } from "url";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/client";
const querySchema = z.object({
uid: z.string(),
seatReferenceUid: z.string().optional(),
rescheduledBy: z.string().optional(),
allowRescheduleForCancelledBooking: z
.string()
.transform((value) => value === "true")
.optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
const {
uid: bookingUid,
seatReferenceUid,
rescheduledBy,
/**
* This is for the case of request-reschedule where the booking is cancelled
*/
allowRescheduleForCancelledBooking,
} = querySchema.parse(context.query);
const coepFlag = context.query["flag.coep"];
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
prisma,
bookingUid
);
const booking = await prisma.booking.findUnique({
where: {
uid,
},
select: {
...bookingMinimalSelect,
eventType: {
select: {
users: {
select: {
username: true,
},
},
slug: true,
team: {
select: {
parentId: true,
slug: true,
},
},
seatsPerTimeSlot: true,
userId: true,
owner: {
select: {
id: true,
},
},
hosts: {
select: {
user: {
select: {
id: true,
},
},
},
},
},
},
dynamicEventSlugRef: true,
dynamicGroupSlugRef: true,
user: true,
status: true,
},
});
const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";
if (!booking) {
return {
notFound: true,
} as const;
}
// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
if (
!allowRescheduleForCancelledBooking &&
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED)
) {
return {
redirect: {
destination: `/booking/${uid}`,
permanent: false,
},
};
}
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
// TODO: Show something in UI to let user know that this booking is not rescheduleable
return {
notFound: true,
} as {
notFound: true;
};
}
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
const userId = session?.user?.id;
if (!userId && !seatReferenceUid) {
return {
redirect: {
destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
permanent: false,
},
};
}
const userIsHost = booking?.eventType.hosts.find((host) => {
if (host.user.id === userId) return true;
});
const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
if (!userIsHost && !userIsOwnerOfEventType) {
return {
notFound: true,
} as {
notFound: true;
};
}
}
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const enrichedBookingUser = booking.user
? await UserRepository.enrichUserWithItsProfile({ user: booking.user })
: null;
const eventUrl = await buildEventUrlFromBooking({
eventType,
dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null,
profileEnrichedBookingUser: enrichedBookingUser,
});
const destinationUrlSearchParams = new URLSearchParams();
destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid);
// TODO: I think we should just forward all the query params here including coep flag
if (coepFlag) {
destinationUrlSearchParams.set("flag.coep", coepFlag as string);
}
const currentUserEmail = rescheduledBy ?? session?.user?.email;
if (currentUserEmail) {
destinationUrlSearchParams.set("rescheduledBy", currentUserEmail);
}
return {
redirect: {
destination: `${eventUrl}?${destinationUrlSearchParams.toString()}${
eventType.seatsPerTimeSlot ? "&bookingUid=null" : ""
}`,
permanent: false,
},
};
}
@@ -34,7 +34,10 @@ const AppsRouting = {
};
function getRoute(appName: string, pages: string[]) {
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record<string, AppPageType>;
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as unknown as Record<
string,
AppPageType
>;
if (!routingConfig) {
return {
+147
View File
@@ -0,0 +1,147 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
import ChooseLicense from "@components/setup/ChooseLicense";
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import type { getServerSideProps } from "@server/lib/setup/getServerSideProps";
function useSetStep() {
const router = useRouter();
const searchParams = useCompatSearchParams();
const pathname = usePathname();
const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.set("step", newStep.toString());
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return setStep;
}
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export function Setup(props: PageProps) {
const { t } = useLocale();
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = useSetStep();
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: (setIsPending) => (
<AdminUser
onSubmit={() => {
setIsPending(true);
}}
onSuccess={() => {
setStep(2);
}}
onError={() => {
setIsPending(false);
}}
userCount={props.userCount}
/>
),
},
{
title: t("choose_a_license"),
description: t("choose_license_description"),
content: (setIsPending) => {
return (
<ChooseLicense
id="wizard-step-2"
name="wizard-step-2"
value={value}
onChange={setValue}
onSubmit={() => {
setIsPending(true);
setStep(3);
}}
/>
);
},
},
];
if (!isFreeLicense) {
steps.push({
title: t("step_enterprise_license"),
description: t("step_enterprise_license_description"),
content: (setIsPending) => {
const currentStep = 3;
return (
<EnterpriseLicense
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
onSubmit={() => {
setIsPending(true);
}}
onSuccess={() => {
setStep(currentStep + 1);
}}
onSuccessValidate={() => {
setIsEnabledEE(true);
}}
/>
);
},
isEnabled: isEnabledEE,
});
}
steps.push({
title: t("enable_apps"),
description: t("enable_apps_description", { appName: APP_NAME }),
contentClassname: "!pb-0 mb-[-1px]",
content: (setIsPending) => {
const currentStep = isFreeLicense ? 3 : 4;
return (
<AdminAppsList
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
classNames={{
form: "mb-4 rounded-md bg-default px-0 pt-0 md:max-w-full",
appCategoryNavigationContainer: "max-h-[400px] overflow-y-auto md:p-4",
verticalTabsItem: "!w-48 md:p-4",
}}
baseURL={`/auth/setup?step=${currentStep}`}
useQueryParam={true}
onSubmit={() => {
setIsPending(true);
router.replace("/");
}}
/>
);
},
});
return (
<main className="bg-subtle flex items-center print:h-full md:h-screen">
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
);
}
Setup.isThemeSupported = false;
export default Setup;
+29
View File
@@ -0,0 +1,29 @@
"use client";
import { signIn } from "next-auth/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button } from "@calcom/ui";
import type { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
function Signin({ providers }: PageProps) {
if (!providers) {
return null;
}
return (
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
{Object.values(providers).map((provider) => {
return (
<div key={provider.name}>
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
</div>
);
})}
</div>
);
}
export default Signin;
+40
View File
@@ -0,0 +1,40 @@
"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
// Accessible only on self-hosted Cal.com instances.
export type SSODirectPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Page({ samlTenantID, samlProductID }: SSODirectPageProps) {
const router = useRouter();
useEffect(() => {
if (HOSTED_CAL_FEATURES) {
router.push("/auth/login");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// Initiate SAML authentication flow
signIn(
"saml",
{
callbackUrl: "/",
},
{ tenant: samlTenantID, product: samlProductID }
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}
@@ -0,0 +1,41 @@
"use client";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Provider(props: SSOProviderPageProps) {
const searchParams = useCompatSearchParams();
const router = useRouter();
useEffect(() => {
const email = searchParams?.get("email");
if (props.provider === "saml") {
if (!email) {
router.push(`/auth/error?error=Email not provided`);
return;
}
if (!props.isSAMLLoginEnabled) {
router.push(`/auth/error?error=SAML login not enabled`);
return;
}
signIn("saml", {}, { tenant: props.tenant, product: props.product });
} else if (props.provider === "google" && email) {
signIn("google", {}, { login_hint: email });
} else {
signIn(props.provider);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}
+43
View File
@@ -0,0 +1,43 @@
"use client";
import { Booker } from "@calcom/atoms/monorepo";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
export default function Type({
slug,
isEmbed,
user,
booking,
isBrandingHidden,
isTeamEvent,
entity,
duration,
hashedLink,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
entity={entity}
/>
<Booker
username={user}
eventSlug={slug}
bookingData={booking}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
entity={entity}
duration={duration}
hashedLink={hashedLink}
/>
</main>
);
}
Type.isBookingPage = true;
@@ -1,38 +1,11 @@
"use client";
import { EventType } from "@calcom/features/eventtypes/components/EventType";
import { EventTypeAppDir } from "@calcom/features/eventtypes/components/EventTypeAppDir";
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
/* eslint-disable @typescript-eslint/no-empty-function */
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import { trpc } from "@calcom/trpc/react";
import { EventType } from "@calcom/atoms/monorepo";
import type { PageProps } from "@lib/event-types/[type]/getServerSideProps";
const EventTypePageWrapper = (props: PageProps & { isAppDir?: boolean }) => {
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (!data) return null;
const eventType = data.eventType;
const { data: workflows } = trpc.viewer.workflows.getAllActiveWorkflows.useQuery({
eventType: {
id: props.type,
teamId: eventType.teamId,
userId: eventType.userId,
parent: eventType.parent,
metadata: eventType.metadata,
},
});
const propsData = {
...(data as EventTypeSetupProps),
allActiveWorkflows: workflows,
};
return props.isAppDir ? <EventTypeAppDir {...propsData} /> : <EventType {...propsData} />;
const EventTypePageWrapper = ({ type, ...rest }: PageProps) => {
return <EventType {...rest} id={type} isAppDir={false} />;
};
export default EventTypePageWrapper;
+120
View File
@@ -0,0 +1,120 @@
"use client";
import {
AverageEventDurationChart,
BookingKPICards,
BookingStatusLineChart,
LeastBookedTeamMembersTable,
MostBookedTeamMembersTable,
PopularEventsTable,
HighestNoShowHostTable,
RecentFeedbackTable,
HighestRatedMembersTable,
LowestRatedMembersTable,
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { Button, ButtonGroup } from "@calcom/ui";
import { Icon } from "@calcom/ui";
import type { getServerSideProps } from "@lib/insights/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function InsightsPage() {
const { t } = useLocale();
const { data: user } = trpc.viewer.me.useQuery();
const features = [
{
icon: <Icon name="users" className="h-5 w-5" />,
title: t("view_bookings_across"),
description: t("view_bookings_across_description"),
},
{
icon: <Icon name="refresh-ccw" className="h-5 w-5" />,
title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"),
},
{
icon: <Icon name="user-plus" className="h-5 w-5" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
];
return (
<div>
<Shell
withoutMain={false}
heading="Insights"
subtitle={t("insights_subtitle")}
title="Insights"
description="View booking insights across your events.">
<UpgradeTip
plan="team"
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
features={features}
background="/tips/insights"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/insights" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
{!user ? (
<></>
) : (
<FiltersProvider>
<Filters />
<div className="mb-4 space-y-4">
<BookingKPICards />
<BookingStatusLineChart />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<PopularEventsTable />
<AverageEventDurationChart />
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<MostBookedTeamMembersTable />
<LeastBookedTeamMembersTable />
</div>
<RecentFeedbackTable />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<HighestNoShowHostTable />
<HighestRatedMembersTable />
<LowestRatedMembersTable />
</div>
<small className="text-default block text-center">
{t("looking_for_more_insights")}{" "}
<a
className="text-blue-500 hover:underline"
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
{" "}
{t("contact_support")}
</a>
</small>
</div>
</FiltersProvider>
)}
</UpgradeTip>
</Shell>
</div>
);
}
@@ -0,0 +1,67 @@
"use client";
import { useRouter } from "next/navigation";
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout } from "@calcom/ui";
import { Button, showToast } from "@calcom/ui";
export const CreateTeamEventType = () => {
const searchParams = useCompatSearchParams();
const { t } = useLocale();
const router = useRouter();
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
const onSuccessMutation = () => {
router.push(`/settings/teams/${teamId}/profile`);
};
const onErrorMutation = (err: string) => {
showToast(err, "error");
};
const SubmitButton = (isPending: boolean) => {
return (
<Button
data-testid="finish-button"
type="submit"
color="primary"
className="w-full justify-center"
disabled={isPending}>
{t("finish")}
</Button>
);
};
return (
<TeamEventTypeForm
isTeamAdminOrOwner={true}
teamId={teamId}
SubmitButton={SubmitButton}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
isInfiniteScrollEnabled={false}
/>
);
};
export const GetLayout = (page: React.ReactElement) => {
const router = useRouter();
const searchParams = useCompatSearchParams();
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
return (
<WizardLayout
currentStep={3}
maxSteps={3}
isOptionalCallback={() => {
router.push(`/settings/teams/${teamId}/profile`);
}}>
{page}
</WizardLayout>
);
};
export default CreateTeamEventType;
@@ -0,0 +1,12 @@
"use client";
import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers";
import { WizardLayout } from "@calcom/ui";
export const GetLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={2} maxSteps={3}>
{page}
</WizardLayout>
);
export default AddNewTeamMembers;
@@ -0,0 +1,62 @@
"use client";
import { useRouter } from "next/navigation";
import React from "react";
import { z } from "zod";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RouterOutputs } from "@calcom/trpc/react";
import { WizardLayout } from "@calcom/ui";
const querySchema = z.object({
returnTo: z.string().optional(),
slug: z.string().optional(),
});
const CreateNewTeamPage = () => {
const params = useParamsWithFallback();
const parsedQuery = querySchema.safeParse(params);
const router = useRouter();
const telemetry = useTelemetry();
const isTeamBillingEnabledClient = !!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && HOSTED_CAL_FEATURES;
const flag = isTeamBillingEnabledClient
? {
telemetryEvent: telemetryEventTypes.team_checkout_session_created,
submitLabel: "checkout",
}
: {
telemetryEvent: telemetryEventTypes.team_created,
submitLabel: "continue",
};
const returnToParam =
(parsedQuery.success ? getSafeRedirectUrl(parsedQuery.data.returnTo) : "/teams") || "/teams";
const onSuccess = (data: RouterOutputs["viewer"]["teams"]["create"]) => {
telemetry.event(flag.telemetryEvent);
router.push(data.url);
};
return (
<CreateANewTeamForm
slug={parsedQuery.success ? parsedQuery.data.slug : ""}
onCancel={() => router.push(returnToParam)}
submitLabel={flag.submitLabel}
onSuccess={onSuccess}
/>
);
};
export const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={1} maxSteps={3}>
{page}
</WizardLayout>
);
};
export default CreateNewTeamPage;
+661
View File
@@ -0,0 +1,661 @@
"use client";
import { Analytics as DubAnalytics } from "@dub/analytics/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { Trans } from "next-i18next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Script from "next/script";
import { useState, useEffect } from "react";
import type { SubmitHandler } from "react-hook-form";
import { useForm, useFormContext } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import { z } from "zod";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib";
import {
APP_NAME,
URL_PROTOCOL_REGEX,
IS_CALCOM,
IS_EUROPE,
WEBAPP_URL,
CLOUDFLARE_SITE_ID,
WEBSITE_PRIVACY_POLICY_URL,
WEBSITE_TERMS_URL,
WEBSITE_URL,
} from "@calcom/lib/constants";
import { isENVDev } from "@calcom/lib/env";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { pushGTMEvent } from "@calcom/lib/gtm";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import {
Button,
HeadSeo,
PasswordField,
TextField,
Form,
Alert,
showToast,
CheckboxField,
Icon,
} from "@calcom/ui";
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
const signupSchema = apiSignupSchema.extend({
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
cfToken: z.string().optional(),
});
const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false });
type FormValues = z.infer<typeof signupSchema>;
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
const FEATURES = [
{
title: "connect_all_calendars",
description: "connect_all_calendars_description",
i18nOptions: {
appName: APP_NAME,
},
icon: "calendar-heart" as const,
},
{
title: "set_availability",
description: "set_availbility_description",
icon: "users" as const,
},
{
title: "share_a_link_or_embed",
description: "share_a_link_or_embed_description",
icon: "link-2" as const,
i18nOptions: {
appName: APP_NAME,
},
},
];
function UsernameField({
username,
setPremium,
premium,
setUsernameTaken,
orgSlug,
usernameTaken,
disabled,
...props
}: React.ComponentProps<typeof TextField> & {
username: string;
setPremium: (value: boolean) => void;
premium: boolean;
usernameTaken: boolean;
orgSlug?: string;
setUsernameTaken: (value: boolean) => void;
}) {
const { t } = useLocale();
const { register, formState } = useFormContext<FormValues>();
const debouncedUsername = useDebounce(username, 600);
useEffect(() => {
if (formState.isSubmitting || formState.isSubmitSuccessful) return;
async function checkUsername() {
// If the username can't be changed, there is no point in doing the username availability check
if (disabled) return;
if (!debouncedUsername) {
setPremium(false);
setUsernameTaken(false);
return;
}
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
setPremium(data.premium);
setUsernameTaken(!data.available);
});
}
checkUsername();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]);
return (
<div>
<TextField
disabled={disabled}
{...props}
{...register("username")}
data-testid="signup-usernamefield"
addOnFilled={false}
/>
{(!formState.isSubmitting || !formState.isSubmitted) && (
<div className="text-gray text-default flex items-center text-sm">
<div className="text-sm ">
{usernameTaken ? (
<div className="text-error flex items-center">
<Icon name="info" className="mr-1 inline-block h-4 w-4" />
<p>{t("already_in_use_error")}</p>
</div>
) : premium ? (
<div data-testid="premium-username-warning" className="flex items-center">
<Icon name="star" className="mr-1 inline-block h-4 w-4" />
<p>
{t("premium_username", {
price: getPremiumPlanPriceValue(),
})}
</p>
</div>
) : null}
</div>
</div>
)}
</div>
);
}
function addOrUpdateQueryParam(url: string, key: string, value: string) {
const separator = url.includes("?") ? "&" : "?";
const param = `${key}=${encodeURIComponent(value)}`;
return `${url}${separator}${param}`;
}
export default function Signup({
prepopulateFormValues,
token,
orgSlug,
isGoogleLoginEnabled,
isSAMLLoginEnabled,
orgAutoAcceptEmail,
redirectUrl,
emailVerificationEnabled,
}: SignupProps) {
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const searchParams = useCompatSearchParams();
const telemetry = useTelemetry();
const { t, i18n } = useLocale();
const router = useRouter();
const formMethods = useForm<FormValues>({
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues satisfies FormValues,
mode: "onChange",
});
const {
register,
watch,
formState: { isSubmitting, errors, isSubmitSuccessful },
} = formMethods;
useEffect(() => {
if (redirectUrl) {
localStorage.setItem("onBoardingRedirect", redirectUrl);
}
}, [redirectUrl]);
const [COOKIE_CONSENT, setCOOKIE_CONSENT] = useState(false);
function handleConsentChange(consent: boolean) {
setCOOKIE_CONSENT(!consent);
}
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
const handleErrorsAndStripe = async (resp: Response) => {
if (!resp.ok) {
const err = await resp.json();
if (err.checkoutSessionId) {
const stripe = await getStripe();
if (stripe) {
console.log("Redirecting to stripe checkout");
const { error } = await stripe.redirectToCheckout({
sessionId: err.checkoutSessionId,
});
console.warn(error.message);
}
} else {
throw new Error(err.message);
}
}
};
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");
const signUp: SubmitHandler<FormValues> = async (_data) => {
const { cfToken, ...data } = _data;
await fetch("/api/auth/signup", {
body: JSON.stringify({
...data,
language: i18n.language,
token,
}),
headers: {
"Content-Type": "application/json",
"cf-access-token": cfToken ?? "invalid-token",
},
method: "POST",
})
.then(handleErrorsAndStripe)
.then(async () => {
if (process.env.NEXT_PUBLIC_GTM_ID)
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started";
const gettingStartedWithPlatform = "settings/platform/new";
const constructCallBackIfUrlPresent = () => {
if (isOrgInviteByLink) {
return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`;
}
return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup");
};
const constructCallBackIfUrlNotPresent = () => {
if (!!isPlatformUser) {
return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`;
}
return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`;
};
const constructCallBackUrl = () => {
const callbackUrlSearchParams = searchParams?.get("callbackUrl");
return !!callbackUrlSearchParams
? constructCallBackIfUrlPresent()
: constructCallBackIfUrlNotPresent();
};
const callBackUrl = constructCallBackUrl();
await signIn<"credentials">("credentials", {
...data,
callbackUrl: callBackUrl,
});
})
.catch((err) => {
formMethods.setError("apiError", { message: err.message });
});
};
return (
<>
{IS_CALCOM && (!IS_EUROPE || COOKIE_CONSENT) ? (
<>
{process.env.NEXT_PUBLIC_GTM_ID && (
<>
<Script
id="gtm-init-script"
dangerouslySetInnerHTML={{
__html: `(function (w, d, s, l, i) {
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
}}
/>
<noscript
dangerouslySetInnerHTML={{
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
}}
/>
</>
)}
<DubAnalytics
cookieOptions={{
domain: isENVDev ? undefined : `.${new URL(WEBSITE_URL).hostname}`,
}}
/>
</>
) : null}
<div
className={classNames(
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
"[--cal-brand-subtle:#9CA3AF]",
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
)}>
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
{/* Left side */}
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
<div className="flex flex-col gap-2">
<h1 className="font-cal text-[28px] leading-none ">
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
</h1>
{IS_CALCOM ? (
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
) : (
<p className="text-subtle text-base font-medium leading-5">
{t("calcom_explained", {
appName: APP_NAME,
})}
</p>
)}
</div>
{/* Form Container */}
<div className="mt-12">
<Form
className="flex flex-col gap-4"
form={formMethods}
handleSubmit={async (values) => {
let updatedValues = values;
if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
updatedValues = {
...values,
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
};
}
await signUp(updatedValues);
}}>
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
usernameTaken={usernameTaken}
disabled={!!orgSlug}
setUsernameTaken={(value) => setUsernameTaken(value)}
data-testid="signup-usernamefield"
setPremium={(value) => setPremiumUsername(value)}
addOnLeading={
orgSlug
? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(URL_PROTOCOL_REGEX, "")}/`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
}
/>
) : null}
{/* Email */}
<TextField
{...register("email")}
label={t("email")}
type="email"
disabled={prepopulateFormValues?.email}
data-testid="signup-emailfield"
/>
{/* Password */}
<PasswordField
data-testid="signup-passwordfield"
label={t("password")}
{...register("password")}
hintErrors={["caplow", "min", "num"]}
/>
{/* Cloudflare Turnstile Captcha */}
{CLOUDFLARE_SITE_ID ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
formMethods.setValue("cfToken", token);
}}
/>
) : null}
<CheckboxField
onChange={() => handleConsentChange(COOKIE_CONSENT)}
description={t("cookie_consent_checkbox")}
/>
{errors.apiError && (
<Alert
className="mb-3"
severity="error"
message={errors.apiError?.message}
data-testid="signup-error-message"
/>
)}
<Button
type="submit"
className="my-2 w-full justify-center"
loading={loadingSubmitState}
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("password") ||
(CLOUDFLARE_SITE_ID &&
!process.env.NEXT_PUBLIC_IS_E2E &&
!formMethods.getValues("cfToken")) ||
isSubmitting ||
usernameTaken
}>
{premiumUsername && !usernameTaken
? `Create Account for ${getPremiumPlanPriceValue()}`
: t("create_account")}
</Button>
</Form>
{!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : (
<div className="mt-6">
<div className="relative flex items-center">
<div className="border-subtle flex-grow border-t" />
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
{t("or_continue_with")}
</span>
<div className="border-subtle flex-grow border-t" />
</div>
</div>
)}
<div className="mt-6 flex flex-col gap-2 md:flex-row">
{isGoogleLoginEnabled ? (
<Button
color="secondary"
disabled={!!formMethods.formState.errors.username || premiumUsername}
loading={isGoogleLoading}
CustomStartIcon={
<img
className={classNames(
"text-subtle mr-2 h-4 w-4 dark:invert",
premiumUsername && "opacity-50"
)}
src="/google-icon.svg"
alt=""
/>
}
className={classNames(
"w-full justify-center rounded-md text-center",
formMethods.formState.errors.username ? "opacity-50" : ""
)}
onClick={async () => {
setIsGoogleLoading(true);
const username = formMethods.getValues("username");
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
const searchQueryParams = new URLSearchParams();
if (username) {
// If username is present we save it in query params to check for premium
searchQueryParams.set("username", username);
localStorage.setItem("username", username);
}
if (token) {
searchQueryParams.set("email", prepopulateFormValues?.email);
}
const url = searchQueryParams.toString()
? `${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`
: GOOGLE_AUTH_URL;
router.push(url);
}}>
Google
</Button>
) : null}
{isSAMLLoginEnabled ? (
<Button
color="secondary"
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
premiumUsername ||
isSubmitting ||
isGoogleLoading
}
className={classNames(
"w-full justify-center rounded-md text-center",
formMethods.formState.errors.username && formMethods.formState.errors.email
? "opacity-50"
: ""
)}
onClick={() => {
if (!formMethods.getValues("username")) {
formMethods.trigger("username");
}
if (!formMethods.getValues("email")) {
formMethods.trigger("email");
return;
}
const username = formMethods.getValues("username");
if (!username) {
showToast("error", t("username_required"));
return;
}
localStorage.setItem("username", username);
const sp = new URLSearchParams();
// @NOTE: don't remove username query param as it's required right now for stripe payment page
sp.set("username", username);
sp.set("email", formMethods.getValues("email"));
router.push(
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
);
}}>
<Icon name="shield-check" className="mr-2 h-5 w-5" />
{t("saml_sso")}
</Button>
) : null}
</div>
</div>
{/* Already have an account & T&C */}
<div className="mt-10 flex h-full flex-col justify-end pb-6 text-xs">
<div className="flex flex-col text-sm">
<div className="flex gap-1">
<p className="text-subtle">{t("already_have_account")}</p>
<Link href="/auth/login" className="text-emphasis hover:underline">
{t("sign_in")}
</Link>
</div>
<div className="text-subtle ">
<Trans
i18nKey="signing_up_terms"
components={[
<Link
className="text-emphasis hover:underline"
key="terms"
href={`${WEBSITE_TERMS_URL}`}
target="_blank">
Terms
</Link>,
<Link
className="text-emphasis hover:underline"
key="privacy"
href={`${WEBSITE_PRIVACY_POLICY_URL}`}
target="_blank">
Privacy Policy.
</Link>,
]}
/>
</div>
</div>
</div>
</div>
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
{IS_CALCOM && (
<>
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
<div>
<img
src="/product-cards/product-of-the-day.svg"
className="h-[34px] w-full dark:invert"
alt="Cal.com was Product of the Day at ProductHunt"
/>
</div>
<div>
<img
src="/product-cards/product-of-the-week.svg"
className="h-[34px] w-full dark:invert"
alt="Cal.com was Product of the Week at ProductHunt"
/>
</div>
<div>
<img
src="/product-cards/product-of-the-month.svg"
className="h-[34px] w-full dark:invert"
alt="Cal.com was Product of the Month at ProductHunt"
/>
</div>
</div>
<div className="mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
<div>
<img
src="/product-cards/producthunt.svg"
className="h-[54px] w-full"
alt="ProductHunt Rating of 5 Stars"
/>
</div>
<div>
<img
src="/product-cards/google-reviews.svg"
className="h-[54px] w-full"
alt="Google Reviews Rating of 4.7 Stars"
/>
</div>
<div>
<img
src="/product-cards/g2.svg"
className="h-[54px] w-full"
alt="G2 Rating of 4.7 Stars"
/>
</div>
</div>
</>
)}
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
<img
className="hidden dark:block"
src="/mock-event-type-list-dark.svg"
alt="Cal.com Booking Page"
/>
</div>
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
{FEATURES.map((feature) => (
<>
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
<div className="text-emphasis items-center">
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
<span className="text-sm font-medium">{t(feature.title)}</span>
</div>
<div className="text-subtle text-sm">
<p>
{t(
feature.description,
feature.i18nOptions && {
...feature.i18nOptions,
}
)}
</p>
</div>
</div>
</>
))}
</div>
</div>
</div>
<Toaster position="bottom-right" />
</div>
</>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.5.1",
"version": "4.5.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+7 -137
View File
@@ -1,152 +1,22 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Meta, WizardForm } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
import ChooseLicense from "@components/setup/ChooseLicense";
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { Meta } from "@calcom/ui";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
function useSetStep() {
const router = useRouter();
const searchParams = useCompatSearchParams();
const pathname = usePathname();
const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined);
_searchParams.set("step", newStep.toString());
router.replace(`${pathname}?${_searchParams.toString()}`);
};
return setStep;
}
import type { PageProps } from "~/auth/setup-view";
import Setup from "~/auth/setup-view";
export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const Page = (props: PageProps) => {
const { t } = useLocale();
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = useSetStep();
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: (setIsPending) => (
<AdminUser
onSubmit={() => {
setIsPending(true);
}}
onSuccess={() => {
setStep(2);
}}
onError={() => {
setIsPending(false);
}}
userCount={props.userCount}
/>
),
},
{
title: t("choose_a_license"),
description: t("choose_license_description"),
content: (setIsPending) => {
return (
<ChooseLicense
id="wizard-step-2"
name="wizard-step-2"
value={value}
onChange={setValue}
onSubmit={() => {
setIsPending(true);
setStep(3);
}}
/>
);
},
},
];
if (!isFreeLicense) {
steps.push({
title: t("step_enterprise_license"),
description: t("step_enterprise_license_description"),
content: (setIsPending) => {
const currentStep = 3;
return (
<EnterpriseLicense
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
onSubmit={() => {
setIsPending(true);
}}
onSuccess={() => {
setStep(currentStep + 1);
}}
onSuccessValidate={() => {
setIsEnabledEE(true);
}}
/>
);
},
isEnabled: isEnabledEE,
});
}
steps.push({
title: t("enable_apps"),
description: t("enable_apps_description", { appName: APP_NAME }),
contentClassname: "!pb-0 mb-[-1px]",
content: (setIsPending) => {
const currentStep = isFreeLicense ? 3 : 4;
return (
<AdminAppsList
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
classNames={{
form: "mb-4 rounded-md bg-default px-0 pt-0 md:max-w-full",
appCategoryNavigationContainer: "max-h-[400px] overflow-y-auto md:p-4",
verticalTabsItem: "!w-48 md:p-4",
}}
baseURL={`/auth/setup?step=${currentStep}`}
useQueryParam={true}
onSubmit={() => {
setIsPending(true);
router.replace("/");
}}
/>
);
},
});
return (
<>
<Meta title={t("setup")} description={t("setup_description")} />
<main className="bg-subtle flex items-center print:h-full md:h-screen">
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
<Setup {...props} />
</>
);
}
Setup.isThemeSupported = false;
Setup.PageWrapper = PageWrapper;
export default Setup;
};
export default Page;
export { getServerSideProps };
+5 -25
View File
@@ -1,34 +1,14 @@
"use client";
import type { getProviders } from "next-auth/react";
import { signIn } from "next-auth/react";
import { Button } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
function signin({ providers }: { providers: Awaited<ReturnType<typeof getProviders>> }) {
if (!providers) {
return null;
}
import type { PageProps } from "~/auth/signin-view";
import SignIn from "~/auth/signin-view";
return (
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
{Object.values(providers).map((provider) => {
return (
<div key={provider.name}>
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
</div>
);
})}
</div>
);
}
const Page = (props: PageProps) => <SignIn {...props} />;
signin.PageWrapper = PageWrapper;
Page.PageWrapper = PageWrapper;
export default signin;
export default Page;
export { getServerSideProps };

Some files were not shown because too many files have changed in this diff Show More