Compare commits

...
40 Commits
Author SHA1 Message Date
GitHub Actions d7b723c914 chore: release v4.8.13
Post release / release (push) Canceled after 0s
2025-01-06 21:59:03 +00:00
Anik Dhabal BabuandAlex van Andel aad3988d60 fix: Zoho Calendar is allowing bookings during busy schedules (#18473)
* fix: zoho calendar allowing to book busy schedule

* Improved more readable version

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-06 21:00:05 +05:30
c8172ab790 refactor: v2 default booking fields (#18287)
* feat: allow title,notes,reschedule reason inputs

* refactor: v2 event type default booking fields as in web app

* wip: input validation

* input field validation

* fix: tests

* chore: bump libraries

* remove change in docs

* fix input type

* chore: ensure additional notes at the end

* fixup! Merge branch 'main' into lauris/cal-4932-platform-refactor-default-booking-fields

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-01-06 16:49:57 +02:00
657f9dfd41 chore: ignore oAuthClientId for Booker Embed bookings (#18314)
* chore: ignore oAuthClientId for Booker Embed bookings

* remove logs

* chore: isEmbed header and emails

* fixup! chore: isEmbed header and emails

* fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! fixup! chore: isEmbed header and emails

* fixup! fixup! fixup! fixup! fixup! chore: isEmbed header and emails

---------

Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-01-06 16:21:38 +02:00
Calcom BotandReplexica c48b209c04 feat: update translations via @replexica (#18472)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-06 11:13:03 -03:00
Hariom BalharaandPeer Richelsen b9a460d8b4 chore: Bump version for embed (#18285)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 12:42:13 +00:00
a7727b5ca6 feat: shorten sms links using dub (#17925)
* feat: add getshortenLinks function

* feat: use shortenLinks in SMS Worflows

* chore: use getShortenLink and cleanup getShortenLinks

* fix: Duplicate keys error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-06 12:39:54 +00:00
Lauris Skraucis 9b661b75f1 docs: OAuth client redirectUri with wildcard character (#18267) 2025-01-06 09:39:31 -03:00
Kartik SainiandPeer Richelsen 487cc0751d feat: add cancel and reschedule reason variables in workflows (#18305)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 11:56:06 +00:00
Akash Jayan f7f36978e1 fix: initialize useCustomTemplate state based on payloadTemplate prop… (#18257) 2025-01-06 12:37:23 +01:00
Amit SharmaandPeer Richelsen c1dbb7cffd feat: refund policies for payment apps (#18428)
* feat: refund policies for payment apps

* tests for payment refund

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-06 11:36:37 +00:00
Morgan e722e75a6f docs: cal-api-version header example for bookings controller v2 (#18271)
* docs: api v2 cal-api-version header example for bookings controller

* fixup! docs: api v2 cal-api-version header example for bookings controller
2025-01-06 11:36:03 +00:00
Amit SharmaandAnik Dhabal Babu 455572b5f4 feat: Info lost when navigating without warning routing form (#18386)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-06 12:19:58 +01:00
Alex van Andel 2fe4462f62 perf: Use improved checkIfAvailable and remove dupe code (#18463) 2025-01-06 09:53:39 +00:00
Amit Sharma f9dc788007 fix: It's hard to click on the last available Event Duration (#18462) 2025-01-04 15:07:36 +00:00
René Aaron 6e293d2985 fix: improve qr readability of alby payment integration (#18266)
* fix: optimize qr for better readability

* fix: optimize readability
2025-01-04 12:18:31 +01:00
Benny Joo 00334c5610 chore: app router - /signup page (#18151)
* app router - /signup page

* add to config.matcher

* fix
2025-01-03 22:27:03 +00:00
Benny JooandHariom Balhara 2f9c8cb441 chore: app router - /team, /org, /[user] booking pages (excl. embeds) (#18186)
* update env vars

* update middleware

* remove pages router and move pages to /app

* move to /team

* update imports

* fix

* remove pages router and move org pages to /app

* wip

* fix orgSlug/user pages

* fix orgSlug/user/type pages

* remove generateMetadata from embed pages

* fix

* remove pages router for /user pages

* generateMetadata is not needed in embed pages

* remove future/org

* no layout in [user] page

* simplify

* fix

* fix OG image for [user]

* fix OG images for org/user and team/slug

* fix OG images for booking page

* fix all metadata

* use isBrandingHidden

* remove BookerSeo and its usages

* rename excludeAppNameFromTitle -> hideBranding

* remove logic for meeting type in HeadSeo

* remove BookerSeo instances from team-view and users-public-view

* create generateMeetingMetadata util and use it to reduce duplicate code

* remove BookerSeo imports

* fix spacing

* remove constructMeetingImage mock from head-seo.test

* fetch avatarUrl using user id for user page metadata

* fix test

* remove unused test cases

* index and follow must be true by default

* invert noindex/nofollow flags

* remove HeadSeo for already migrated pages and refactor prepareMetadata

* fix organization-settings.e2e.ts

* fix order

* enable parallel test execution for dynamic-booking e2e test

* fix

* + could be %2B in app router

* refactor handling logic for embeds in app router

* fix isEmbed

* fix embed-core

* remove dead code

* move embed pages back to /future

* add back embed pages in pages router

* revert some changes

* fix import type checks

* simplify

* fix

* feat: Implement generateBookingPageMetadata function for improved SEO and metadata handling across user and event pages (#18440)

- Added a new utility function `generateBookingPageMetadata` to streamline the generation of metadata for booking and user profile pages.
- Updated multiple page components to utilize the new function, enhancing SEO indexing and metadata consistency.
- Removed redundant code related to previous metadata generation methods, improving code clarity and maintainability.

* fix dirs

* Pr-review-fixes-app-router-team-pages (#18450)

* Remove unnecessary getPublicEvent call

* Remove unused variable

* Fix 404 for team page (#18451)

* Remove console log

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-01-03 22:26:30 +00:00
GitHub Actions 39173900b1 chore: release v4.8.12
Post release / release (push) Canceled after 0s
2025-01-03 21:53:06 +00:00
Syed Ali Shahbaz bd61e01a5d fix: Show managed event bookings for team admins/owners (#18434)
* add managed event bookings for team admins/owners

* remove unnecessary OR

* address change suggestion

* improve readability
2025-01-03 21:43:05 +00:00
Cao Hoang aa0b3a2778 fix: improve useInViewObserver hook to trigger when observer is already in view on mount (#18455) 2025-01-03 21:20:59 +00:00
Joe Au-Yeung 170feeae5e chore: Salesforce - Add additional logging and error checks (#18448) 2025-01-03 12:57:24 -05:00
Eunjae Lee a67b57b216 feat: server side sorting for insights routing (#18113)
* feat: server side sorting for insights routing

^ Conflicts:
^	packages/prisma/playground.ts

* fix download csv

* fix hooks locations
2025-01-03 10:58:01 +00:00
821a16648f fix: Timezone in Email Embed not working #15525 (#15537)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2025-01-03 08:43:09 +00:00
Hariom Balhara 727f7df2c2 fix: Enhance SCIM payload handling and attribute creation (#18427)
* feat: Enhance SCIM user attribute handling and sync process

- Introduced a new PR_TODO.md file to track ongoing tasks and fixes.
- Updated `getAttributesFromScimPayload` to accept a directoryId and ignore core user attributes during syncing.
- Modified `handleUserEvents` to pass directoryId when syncing custom attributes.
- Improved attribute creation logic to handle unique options and added support for optional isGroup property in attribute options.
- Added utility function `getOptionsWithValidContains` to ensure valid sub-options in attribute options.
- Updated tests to reflect changes in attribute handling and ensure proper functionality.

This commit addresses issues with user attribute syncing and enhances the overall attribute management process.

* test: Add unit tests for getOptionsWithValidContains utility function

- Introduced a new test file for the getOptionsWithValidContains function.
- Added tests to verify the removal of duplicate options, initialization of empty contains arrays, filtering of non-existent sub-options, and handling of empty options arrays.
- Ensured comprehensive coverage of the utility's functionality to enhance reliability and maintainability.
2025-01-03 13:36:21 +05:30
Anik Dhabal Babu be5f7f83b5 fix: Zoho Bigin not pulling information from booking (#18445)
* fix: Zoho Bigin not pulling information from booking

* update

* missed

* fix contactcreation
2025-01-03 08:41:18 +05:30
amrit 02a49e2b4b fix: react incompatiblity with nextjs error (#18433) 2025-01-03 01:03:46 +00:00
Cao Hoang 22f92f1ee3 fix: improve useInViewObserver hook to trigger when observer is already in view on mount (#18446) 2025-01-02 23:58:40 +00:00
Udit Takkar c7a797c0e0 fix: get bookings ordering (#18439)
* fix: get bookings ordering

* chore: update query

* chore: add log for debuggin on prod

* chore
2025-01-02 23:41:30 +00:00
Calcom BotandReplexica 447436dfff feat: update translations via @replexica (#18438)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-02 22:23:30 +00:00
Eunjae LeeandUdit Takkar c415b2d7f5 feat: apply full width automatically for DataTable (#18357)
* feat: apply full width automatically for DataTable

* change implementation

* load all columns of insights routing table at the same time

* update team member list

* sticky columns for >= sm

* fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-02 14:32:39 +00:00
Lauris Skraucis c10c87c98e fix: booker atom booking fields (#18441) 2025-01-02 11:31:43 +00:00
Benny Joo 85f96f492d chore: migrate /icons page to App Router (#18436)
* migrate icons page

* migrate main index page

* remove /icons page

* use i18n string

* fix imports

* add icons_showcase i18n string

* add /app dir to tailwind preset config

* use classname

* do not migrate main index page
2025-01-02 15:18:19 +05:30
Morgan b1350d0764 fix: atoms globals.css invalid class format (#18295) 2025-01-02 09:32:50 +00:00
Hariom BalharaandUdit Takkar bac2d1f6f0 feat: Add dry run mode functionality and message display in booking process (#18429)
- Introduced a new `DryRunMessage` component to inform users when in dry run mode.
- Implemented utility function `isBookingDryRun` to check for dry run status based on URL search parameters.
- Updated the `Booker` component to conditionally render the dry run message.
- Adjusted booking mutation input mapping to reuse the utility

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-01-01 11:40:53 +00:00
Anik Dhabal Babu fec504496a fix: Add to google leads to a 403 page (#18430) 2024-12-31 14:11:42 +00:00
Alex van Andel 890fc92148 fix: Some recently discovered type errors breaking build (not sure why undetected before) (#18432) 2024-12-31 13:50:43 +00:00
Benny Joo 557bd8bf25 chore: Migrate /403 and /500 pages to App Router (#18424)
* global-error is not needed

* remove /403 and /500 pages from pages router

* add 403 and 500 pages in App Router

* add i18n strings

* fix i18n string

* fix styling
2024-12-31 11:43:15 +05:30
Cao HoangandUdit Takkar d8027fe248 Fix duplicated data returned by Prisma query in team availability view (#18416)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-12-30 21:15:27 +00:00
Anik Dhabal Babu 116c765c36 fix: Error changing location for meeting (#18401) 2024-12-30 14:59:07 +00:00
272 changed files with 5360 additions and 2560 deletions
-1
View File
@@ -365,7 +365,6 @@ 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_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
APP_ROUTER_AUTH_LOGIN_ENABLED=0
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+1 -1
View File
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.79",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+2
View File
@@ -18,6 +18,7 @@ import {
CAL_API_VERSION_HEADER,
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
@@ -45,6 +46,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
allowedHeaders: [
X_CAL_CLIENT_ID,
X_CAL_SECRET_KEY,
X_CAL_PLATFORM_EMBED,
CAL_API_VERSION_HEADER,
"Accept",
"Authorization",
@@ -37,7 +37,7 @@ import { Request } from "express";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { X_CAL_CLIENT_ID, X_CAL_PLATFORM_EMBED } from "@calcom/platform-constants";
import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants";
import {
handleNewRecurringBooking,
@@ -161,14 +161,15 @@ export class BookingsController_2024_04_15 {
async createBooking(
@Req() req: BookingRequest,
@Body() body: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed)
);
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
@@ -192,13 +193,16 @@ export class BookingsController_2024_04_15 {
@Req() req: BookingRequest,
@Param("bookingId") bookingId: string,
@Body() _: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
const oAuthClientId = clientId?.toString();
if (bookingId) {
try {
req.body.id = parseInt(bookingId);
const res = await handleCancelBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
const res = await handleCancelBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
if (!res.onlyRemovedAttendee) {
void (await this.billingService.cancelUsageByBookingUid(res.bookingUid));
}
@@ -246,7 +250,8 @@ export class BookingsController_2024_04_15 {
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId = clientId?.toString();
try {
@@ -258,7 +263,7 @@ export class BookingsController_2024_04_15 {
}
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
await this.createNextApiRecurringBookingRequest(req, oAuthClientId)
await this.createNextApiRecurringBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
createdBookings.forEach(async (booking) => {
@@ -284,13 +289,14 @@ export class BookingsController_2024_04_15 {
async createInstantBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
await this.createNextApiBookingRequest(req, oAuthClientId)
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
);
if (instantMeeting.userId && instantMeeting.bookingUid) {
@@ -332,8 +338,14 @@ export class BookingsController_2024_04_15 {
}
}
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
if (isEmbed) {
// embed should ignore oauth client settings and enable emails by default
return { ...res, arePlatformEmailsEnabled: true };
}
try {
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
// fetch oAuthClient from db and use data stored in db to set these values
@@ -354,13 +366,14 @@ export class BookingsController_2024_04_15 {
private async createNextApiBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
platformBookingLocation?: string,
isEmbed?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const requestId = req.get("X-Request-Id");
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId)
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
this.logger.log(`createNextApiBookingRequest_2024_04_15`, {
requestId,
@@ -377,12 +390,13 @@ export class BookingsController_2024_04_15 {
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
platformBookingLocation?: string
platformBookingLocation?: string,
isEmbed?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId)
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
const requestId = req.get("X-Request-Id");
this.logger.log(`createNextApiRecurringBookingRequest_2024_04_15`, {
@@ -424,4 +438,8 @@ export class BookingsController_2024_04_15 {
throw new InternalServerErrorException(errMsg);
}
private transformToBoolean(v?: string): boolean {
return v && typeof v === "string" ? v.toLowerCase() === "true" : false;
}
}
@@ -5,7 +5,7 @@ import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/out
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -65,6 +65,7 @@ import {
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-08-13\``,
example: VERSION_2024_08_13,
required: true,
})
export class BookingsController_2024_08_13 {
@@ -952,7 +952,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
slug: recurringSeatedTventTypeSlug,
});
expect(firstBooking.attendees.length).toEqual(2);
expect(firstBooking.attendees[0]).toEqual({
const firstAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeOne
);
expect(firstAttendee).toEqual({
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: body.attendee.timeZone,
@@ -965,7 +968,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
expect(firstBooking.attendees[1]).toEqual({
const secondAttendee = firstBooking.attendees.find(
(attendee) => attendee.name === nameAttendeeTwo
);
expect(secondAttendee).toEqual({
name: nameAttendeeTwo,
email: emailAttendeeTwo,
timeZone: body.attendee.timeZone,
@@ -33,11 +33,18 @@ import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
NameFieldInput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { SchedulingType } from "@calcom/prisma/enums";
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
if (a.slug < b.slug) return -1;
if (a.slug > b.slug) return 1;
return 0;
};
describe("Event types Endpoints", () => {
describe("Not authenticated", () => {
let app: INestApplication;
@@ -93,6 +100,68 @@ describe("Event types Endpoints", () => {
let orgUserEventType1: EventType;
let orgUserEventType2: EventType;
let orgUserEventType3: EventType;
const defaultResponseBookingFieldName = {
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldEmail = {
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldLocation = {
isDefault: true,
type: "radioInput",
slug: "location",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldTitle = {
isDefault: true,
type: "text",
slug: "title",
required: true,
hidden: true,
disableOnPrefill: false,
};
const defaultResponseBookingFieldNotes = {
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldGuests = {
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
hidden: false,
disableOnPrefill: false,
};
const defaultResponseBookingFieldRescheduleReason = {
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
hidden: false,
disableOnPrefill: false,
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -253,7 +322,7 @@ describe("Event types Endpoints", () => {
});
it("should create an event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
@@ -379,30 +448,25 @@ describe("Event types Endpoints", () => {
);
expect(createdEventType.color).toEqual(body.color);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
// note(Lauris): location booking field is added if multiple locations are passed
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
{
isDefault: true,
required: false,
slug: "location",
type: "radioInput",
disableOnPrefill: false,
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: true,
hidden: false,
isDefault: false,
},
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldNotes },
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
@@ -432,7 +496,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -455,7 +521,6 @@ describe("Event types Endpoints", () => {
});
it(`/GET/event-types by username and orgSlug`, async () => {
console.log(organization);
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
@@ -636,7 +701,6 @@ describe("Event types Endpoints", () => {
.expect(201);
const createdEventType = createResponse.body.data;
console.log("createdEventType: ", createdEventType);
expect(createdEventType).toMatchObject({
id: expect.any(Number),
@@ -779,13 +843,20 @@ describe("Event types Endpoints", () => {
});
it("should update event type", async () => {
const nameBookingField: NameFieldInput_2024_06_14 = {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
label: "Your name sir / madam",
placeholder: "john doe",
disableOnPrefill: true,
};
const notesBookingField: NotesDefaultFieldInput_2024_06_14 = {
slug: "notes",
label: "lemme take some notes",
placeholder: "write your notes here",
required: true,
};
const newTitle = "Coding class in Italian!";
const body: UpdateEventTypeInput_2024_06_14 = {
@@ -804,6 +875,7 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
notesBookingField,
],
bookingLimitsCount: {
day: 4,
@@ -866,21 +938,25 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
const requestBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{ ...defaultResponseBookingFieldName, ...nameBookingField },
{ ...defaultResponseBookingFieldEmail },
{ ...defaultResponseBookingFieldLocation },
{ ...defaultResponseBookingFieldTitle },
{ ...defaultResponseBookingFieldGuests },
{ ...defaultResponseBookingFieldRescheduleReason },
{
isDefault: true,
required: false,
slug: "rescheduleReason",
type: "textarea",
type: "select",
label: "select which language you want to learn",
slug: "select-language",
required: true,
placeholder: "select language",
options: ["javascript", "python", "cobol"],
disableOnPrefill: false,
hidden: false,
isDefault: false,
},
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
.map((field) => ({ isDefault: false, ...field })),
{ ...defaultResponseBookingFieldNotes, ...notesBookingField },
];
expect(updatedEventType.bookingFields).toEqual(expectedBookingFields);
@@ -997,7 +1073,9 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
@@ -1091,14 +1169,6 @@ describe("Event types Endpoints", () => {
placeholder: "",
},
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
@@ -1116,6 +1186,14 @@ describe("Event types Endpoints", () => {
hidden: false,
},
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
];
beforeAll(async () => {
@@ -1239,15 +1317,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1283,6 +1352,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1369,15 +1447,6 @@ describe("Event types Endpoints", () => {
defaultLabel: "what_is_this_meeting_about",
defaultPlaceholder: "",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
{
name: "guests",
type: "multiemail",
@@ -1413,6 +1482,15 @@ describe("Event types Endpoints", () => {
required: false,
defaultLabel: "phone_number",
},
{
name: "notes",
type: "textarea",
sources: [{ id: "default", type: "default", label: "Default" }],
editable: "system-but-optional",
required: false,
defaultLabel: "additional_notes",
defaultPlaceholder: "share_additional_notes",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1426,19 +1504,21 @@ describe("Event types Endpoints", () => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
]);
expect(fetchedEventType.bookingFields.sort(orderBySlug)).toEqual(
[
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
required: userDefinedBookingField.required,
placeholder: userDefinedBookingField.placeholder,
disableOnPrefill: false,
hidden: false,
},
...expectedReturnSystemFields,
].sort(orderBySlug)
);
});
});
@@ -1541,14 +1621,6 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
@@ -1565,6 +1637,14 @@ describe("Event types Endpoints", () => {
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
]);
});
});
@@ -5,16 +5,19 @@ import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import {
transformBookingFieldsApiRequestToInternal,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldTitle,
systemAfterFieldNotes,
systemAfterFieldGuests,
systemAfterFieldRescheduleReason,
EventTypeMetaDataSchema,
systemBeforeFieldLocation,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
transformEventColorsApiToInternal,
@@ -217,27 +220,46 @@ export class InputEventTypesService_2024_06_14 {
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const customFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
const customFieldsWithoutNameEmail = customFields.filter(
(field) => field.type !== "name" && field.type !== "email"
const systemCustomFields = internalFields.filter((field) => !this.isUserCustomField(field));
const userCustomFields = internalFields.filter((field) => this.isUserCustomField(field));
const systemCustomNameField = systemCustomFields?.find((field) => field.type === "name");
const systemCustomEmailField = systemCustomFields?.find((field) => field.type === "email");
const systemCustomTitleField = systemCustomFields?.find((field) => field.name === "title");
const systemCustomNotesField = systemCustomFields?.find((field) => field.name === "notes");
const systemCustomGuestsField = systemCustomFields?.find((field) => field.name === "guests");
const systemCustomRescheduleReasonField = systemCustomFields?.find(
(field) => field.name === "rescheduleReason"
);
const customNameField = customFields?.find((field) => field.type === "name");
const customEmailField = customFields?.find((field) => field.type === "email");
const defaultFieldsBefore: (SystemField | CustomField)[] = [
customNameField || systemBeforeFieldName,
customEmailField || systemBeforeFieldEmail,
systemCustomNameField || systemBeforeFieldName,
systemCustomEmailField || systemBeforeFieldEmail,
systemBeforeFieldLocation,
];
// 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 defaultFieldsAfter = [systemAfterFieldRescheduleReason];
const defaultFieldsAfter = [
systemCustomTitleField || systemAfterFieldTitle,
systemCustomNotesField || systemAfterFieldNotes,
systemCustomGuestsField || systemAfterFieldGuests,
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...customFieldsWithoutNameEmail, ...defaultFieldsAfter];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
return (
field.type !== "name" &&
field.type !== "email" &&
field.name !== "title" &&
field.name !== "notes" &&
field.name !== "guests" &&
field.name !== "rescheduleReason"
);
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
@@ -242,7 +242,19 @@ export class OutputEventTypesService_2024_06_14 {
}
}
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
const fields = [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
// ensure additional notes are always at the end
return fields.sort((a, b) => {
if (!a?.slug || !b?.slug) return 0;
if (a.slug === "notes" && b.slug !== "notes") {
return 1;
}
if (b.slug === "notes" && a.slug !== "notes") {
return -1;
}
return 0;
});
}
getDefaultBookingFields(isOrgTeamEvent: boolean) {
@@ -41,6 +41,18 @@ describe("isOriginAllowed", () => {
const origin = "https://sub.mydomain.com/path/to/resource";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with wildcard for routes correctly", () => {
const allowedOrigins = ["https://domain.com/*"];
const origin = "https://domain.com/dashboard";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
it("should handle patterns with wildcard for root route correctly", () => {
const allowedOrigins = ["https://domain.com*"];
const origin = "https://domain.com";
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
});
});
describe("is not allowed", () => {
@@ -179,7 +179,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
throw new UnauthorizedException(
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client with ID '${client.id}'`
);
}
@@ -677,27 +677,9 @@ describe("Organizations Event Types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
isDefault: true,
type: "name",
slug: "name",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "email",
slug: "email",
required: true,
disableOnPrefill: false,
},
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: false,
hidden: true,
},
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
{
isDefault: true,
type: "radioInput",
@@ -708,33 +690,33 @@ describe("Organizations Event Types Endpoints", () => {
},
{
isDefault: true,
type: "text",
slug: "title",
required: true,
slug: "title",
type: "text",
disableOnPrefill: false,
hidden: true,
},
{
isDefault: true,
type: "textarea",
slug: "notes",
required: false,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "multiemail",
slug: "guests",
required: false,
type: "multiemail",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "textarea",
slug: "rescheduleReason",
required: false,
slug: "rescheduleReason",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
required: false,
slug: "notes",
type: "textarea",
disableOnPrefill: false,
hidden: false,
},
+274 -82
View File
@@ -7741,7 +7741,7 @@
"type"
]
},
"NameFieldInput_2024_06_14": {
"NameDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7766,7 +7766,7 @@
"placeholder"
]
},
"EmailFieldInput_2024_06_14": {
"EmailDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"type": {
@@ -7791,6 +7791,126 @@
"placeholder"
]
},
"TitleDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "title",
"description": "only allowed value for type is `title`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=journey`, the title field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"NotesDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "notes",
"description": "only allowed value for type is `notes`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=journey`, the notes field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"GuestsDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "guests",
"description": "only allowed value for type is `guests`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=bob@cal.com`, the guests field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"RescheduleReasonDefaultFieldInput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`"
},
"required": {
"type": "boolean"
},
"hidden": {
"type": "boolean",
"description": "If true show under event type settings but don't show this booking field in the Booker. If false show in both."
},
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=travel`, the rescheduleReason field will be prefilled with this value and disabled."
}
},
"required": [
"slug"
]
},
"BookerLayouts_2024_06_14": {
"type": "object",
"properties": {
@@ -7921,10 +8041,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -8475,20 +8607,19 @@
"RescheduleReasonDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "rescheduleReason",
"description": "only allowed value for type is `rescheduleReason`",
"default": "rescheduleReason"
},
"type": {
"type": "string",
"default": "textarea"
},
"required": {
"type": "boolean"
},
@@ -8505,33 +8636,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&rescheduleReason=busy`, the reschedule reason field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "title"
},
"type": {
"type": "string",
"default": "text"
"default": "textarea"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"TitleDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "title",
"description": "only allowed value for type is `title`",
"default": "title"
},
"required": {
"type": "boolean"
@@ -8549,33 +8686,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&title=masterclass`, the title field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "notes"
},
"type": {
"type": "string",
"default": "textarea"
"default": "text"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"NotesDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "notes",
"description": "only allowed value for type is `notes`",
"default": "notes"
},
"required": {
"type": "boolean"
@@ -8593,33 +8736,39 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&notes=hello`, the notes field will be prefilled with this value and disabled."
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"slug": {
"type": "string",
"default": "guests"
},
"type": {
"type": "string",
"default": "multiemail"
"default": "textarea"
}
},
"required": [
"slug",
"isDefault",
"type"
]
},
"GuestsDefaultFieldOutput_2024_06_14": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"enum": [
"name",
"email",
"title",
"notes",
"guests"
],
"example": "guests",
"description": "only allowed value for type is `guests`",
"default": "guests"
},
"required": {
"type": "boolean"
@@ -8637,15 +8786,22 @@
"disableOnPrefill": {
"type": "boolean",
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&guests=lauris@cal.com`, the guests field will be prefilled with this value and disabled."
},
"isDefault": {
"type": "object",
"default": true,
"description": "This property is always true because it's a default field",
"example": true
},
"type": {
"type": "string",
"default": "multiemail"
}
},
"required": [
"isDefault",
"slug",
"type",
"required",
"hidden",
"disableOnPrefill"
"isDefault",
"type"
]
},
"AddressFieldOutput_2024_06_14": {
@@ -9776,10 +9932,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -11353,10 +11521,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
@@ -12178,10 +12358,22 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
"$ref": "#/components/schemas/NameDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
"$ref": "#/components/schemas/EmailDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/TitleDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/NotesDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/GuestsDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/RescheduleReasonDefaultFieldInput_2024_06_14"
},
{
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
+9
View File
@@ -1,5 +1,14 @@
# @calcom/web
## 4.8.7
### Patch Changes
- Updated dependencies
- @calcom/embed-core@1.5.2
- @calcom/embed-react@1.5.2
- @calcom/embed-snippet@1.3.2
## 4.5.2
### Patch Changes
-1
View File
@@ -20,7 +20,6 @@ const ROUTES: [URLPattern, boolean][] = [
["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const,
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,
@@ -1,20 +1,20 @@
import Head from "next/head";
import { _generateMetadata, getTranslate } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export const generateMetadata = () =>
_generateMetadata(
(t) => `${t("access_denied")} | ${APP_NAME}`,
() => ""
);
export default function AccessDenied() {
const { t } = useLocale();
async function Error403() {
const t = await getTranslate();
return (
<div className="bg-subtle flex h-screen">
<Head>
<title>{`${t("access_denied")} | ${APP_NAME}`}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">403</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
@@ -27,4 +27,4 @@ export default function AccessDenied() {
);
}
AccessDenied.PageWrapper = PageWrapper;
export default WithLayout({ ServerPage: Error403 });
+21
View File
@@ -0,0 +1,21 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
export default function CopyButton({ error }: { error: string }) {
const { t } = useLocale();
return (
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon="copy"
onClick={() => {
navigator.clipboard.writeText(error);
showToast("Link copied!", "success");
}}>
{t("copy")}
</Button>
);
}
+47
View File
@@ -0,0 +1,47 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { APP_NAME } from "@calcom/lib/constants";
import { Button } from "@calcom/ui";
import CopyButton from "./copy-button";
export const generateMetadata = () =>
_generateMetadata(
(t) => `${t("something_unexpected_occurred")} | ${APP_NAME}`,
() => ""
);
async function Error500({ searchParams }: { searchParams: { error?: string } }) {
const t = await getTranslate();
return (
<div className="bg-subtle flex h-screen">
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("500_error_message")}</h2>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
{searchParams?.error && (
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
{t("please_provide_following_text_to_suppport")}:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{searchParams.error}
<br />
<CopyButton error={searchParams.error} />
</pre>
</div>
)}
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
{t("go_back")}
</Button>
</div>
</div>
);
}
export default WithLayout({
ServerPage: Error500,
});
+10 -7
View File
@@ -13,6 +13,9 @@ export function createMockNextJsRequest(...args: Parameters<typeof createMocks>)
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
notFound: vi.fn(),
@@ -86,7 +89,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "namespace1",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?layout=week_view&embed=namespace1");
});
@@ -105,7 +108,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "namespace1",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith(
"/reschedule/embed?redirectParam=1&layout=week_view&embed=namespace1"
@@ -126,7 +129,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith("/reschedule/embed?redirectParam=1&layout=week_view&embed=");
});
@@ -147,7 +150,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "namespace1",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith(
"https://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
@@ -168,7 +171,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "namespace1",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith(
"http://calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
@@ -189,7 +192,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "namespace1",
},
})
).catch(() => {});
).catch(noop);
expect(redirect).toHaveBeenCalledWith(
"/calcom.cal.local/owner/embed?layout=week_view&embed=namespace1"
@@ -239,7 +242,7 @@ describe("withEmbedSsrAppDir", () => {
embed: "",
},
})
).catch(() => {});
).catch(noop);
expect(notFound).toHaveBeenCalled();
});
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
@@ -18,7 +18,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { booking, user: username, slug: eventSlug } = props;
const { booking, user: username, slug: eventSlug, isSEOIndexable, eventData, isBrandingHidden } = props;
const rescheduleUid = booking?.uid;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
@@ -31,12 +31,21 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
});
const profileName = event?.profile?.name ?? "";
const profileImage = event?.profile.image;
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
);
const metadata = await generateEventBookingPageMetadata({
event: {
hidden: event?.hidden ?? false,
title,
users: event?.users ?? [],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable: !!isSEOIndexable,
profile: { name: profileName, image: profileImage ?? "" },
isReschedule: !!rescheduleUid,
});
return metadata;
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
+39
View File
@@ -0,0 +1,39 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateUserProfilePageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
import LegacyPage from "~/users/views/users-public-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
const isOrg = !!profile?.organization;
const allowSEOIndexing = !!(
(!isOrg && profile.allowSEOIndexing) ||
(isOrg && isOrgSEOIndexable && profile.allowSEOIndexing)
);
return await generateUserProfilePageMetadata({
profile: {
name: profile.name,
image: profile.image ?? "",
username: profile.username ?? "",
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: entity.orgSlug ?? null,
isSEOIndexable: allowSEOIndexing,
});
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
export default WithLayout({ getData, Page: LegacyPage })<"P">;
+21 -2
View File
@@ -3,7 +3,8 @@ import i18next from "i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { headers } from "next/headers";
import { constructGenericImage } from "@calcom/lib/OgImages";
import type { MeetingImageProps } from "@calcom/lib/OgImages";
import { constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import { truncateOnWord } from "@calcom/lib/text";
@@ -33,7 +34,7 @@ const createI18nInstance = async (locale: string, ns: string) => {
return _i18n;
};
const getTranslationWithCache = async (locale: string, ns: string = "common") => {
const getTranslationWithCache = async (locale: string, ns = "common") => {
const localeWithFallback = locale ?? "en";
const i18n = await createI18nInstance(localeWithFallback, ns);
return i18n.getFixedT(localeWithFallback, ns);
@@ -103,3 +104,21 @@ export const _generateMetadata = async (
},
};
};
export const generateMeetingMetadata = async (
meeting: MeetingImageProps,
getTitle: (t: TFunction<string, undefined>) => string,
getDescription: (t: TFunction<string, undefined>) => string,
hideBranding?: boolean,
origin?: string
) => {
const metadata = await _generateMetadataWithoutImage(getTitle, getDescription, hideBranding, origin);
const image = SEO_IMG_OGIMG + constructMeetingImage(meeting);
return {
...metadata,
openGraph: {
...metadata.openGraph,
images: [image],
},
};
};
+1 -1
View File
@@ -20,7 +20,7 @@ const inputSchema = z.object({
cardKeys: z.array(z.string()),
});
export async function handler(request: Request) {
async function handler(request: Request) {
const headersList = headers();
const requestBody = await request.json();
+1 -1
View File
@@ -1,4 +1,4 @@
import { PageProps } from "app/_types";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { redirect } from "next/navigation";
+3 -2
View File
@@ -17,7 +17,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const pageProps = await getData(legacyCtx);
const { booking, user: username, slug: eventSlug, isTeamEvent } = pageProps;
const { booking, user: username, slug: eventSlug, isTeamEvent, isBrandingHidden } = pageProps;
const rescheduleUid = booking?.uid;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -34,7 +34,8 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
isBrandingHidden
);
};
-27
View File
@@ -1,27 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
import type { PageProps as LegacyPageProps } from "~/users/views/users-public-view";
import LegacyPage from "~/users/views/users-public-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const { profile, markdownStrippedBio } = props;
return await _generateMetadata(
() => profile.name,
() => markdownStrippedBio
);
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
@@ -1,10 +1,9 @@
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { Page, type OrgTypePageProps } from "app/org/[orgSlug]/[user]/[type]/page";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir<OrgTypePageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
@@ -1,9 +0,0 @@
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page });
@@ -1,8 +1,9 @@
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import Page from "@pages/org/[orgSlug]/[user]/embed";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { Page, type OrgPageProps } from "app/org/[orgSlug]/[user]/page";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
const getEmbedData = withEmbedSsrAppDir<OrgPageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, ServerPage: Page });
@@ -1,7 +0,0 @@
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page });
@@ -1,6 +1,4 @@
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
@@ -1,29 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
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 }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -1,45 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
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 }: _PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { user: username, slug: eventSlug, booking } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent: true,
org: isValidOrgDomain ? currentOrgDomain : null,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TypePage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -1,29 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
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 }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: TeamPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -1,6 +1,4 @@
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -1,7 +1,4 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+151
View File
@@ -0,0 +1,151 @@
import type { TFunction } from "next-i18next";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { generateMeetingMetadata } from "./_utils";
type MetadataPayload = {
title: (t: TFunction) => string;
description: (t: TFunction) => string;
meeting: {
title: string;
profile: { name: string; image: string };
users?: { name: string; username: string }[];
};
robots: {
index: boolean;
follow: boolean;
};
};
type UserProfilePageProfile = {
name: string | null;
username: string | null;
image: string | null;
markdownStrippedBio: string | null | undefined;
};
type TeamProfilePageProfile = {
teamName: string | null;
image: string | null;
markdownStrippedBio: string | null | undefined;
};
type EventBookingPageProfile = { name: string; image: string };
type BaseMetadataArgs = {
hideBranding: boolean;
orgSlug: string | null;
isSEOIndexable: boolean;
isReschedule?: boolean;
};
type TeamProfilePageArgs = BaseMetadataArgs & {
profile: TeamProfilePageProfile;
event: null;
};
type UserProfilePageArgs = BaseMetadataArgs & {
profile: UserProfilePageProfile;
event: null;
};
type EventBookingPageArgs = BaseMetadataArgs & {
profile: EventBookingPageProfile;
event: {
title: string;
hidden: boolean;
users: { name: string | null; username: string | null }[];
};
};
export const generateEventBookingPageMetadata = async (props: EventBookingPageArgs) => {
const { profile, isReschedule, hideBranding, orgSlug, isSEOIndexable, event } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title} | ${profile.name}`,
description: (t) => `${isReschedule ? t("reschedule") : ""} ${event.title}`,
meeting: {
title: event.title,
profile: { name: profile.name, image: profile.image },
users: event.users.map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
},
robots: {
index: !(event.hidden || !isSEOIndexable),
follow: !(event.hidden || !isSEOIndexable),
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
export const generateUserProfilePageMetadata = async (props: UserProfilePageArgs) => {
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: () => profile.name ?? "",
description: () => profile.markdownStrippedBio ?? "",
meeting: {
title: profile.markdownStrippedBio ?? "",
profile: { name: profile.name ?? "", image: profile.image ?? "" },
users: [{ name: profile.name ?? "", username: profile.username ?? "" }],
},
robots: {
index: isSEOIndexable,
follow: isSEOIndexable,
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
export const generateTeamProfilePageMetadata = async (props: TeamProfilePageArgs) => {
const { profile, hideBranding, orgSlug, isSEOIndexable } = props;
const fullOrigin = getOrgFullOrigin(orgSlug);
const payload: MetadataPayload = {
title: (t) => profile.teamName || t("nameless_team"),
description: () => profile.markdownStrippedBio ?? "",
meeting: {
title: profile.markdownStrippedBio ?? "",
profile: { name: `${profile.teamName}`, image: profile.image ?? "" },
},
robots: {
index: isSEOIndexable,
follow: isSEOIndexable,
},
};
return {
...(await generateMeetingMetadata(
payload.meeting,
payload.title,
payload.description,
hideBranding,
fullOrigin
)),
robots: payload.robots,
};
};
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { Icon } from "@calcom/ui";
import type { IconName } from "@calcom/ui";
export const IconGrid = (props: {
title: string;
icons: IconName[];
rootClassName?: string;
iconClassName?: string;
}) => (
<div className={props.rootClassName}>
<h2 className="font-cal mt-6 text-lg font-medium">{props.title}</h2>
<div className="grid grid-cols-2 lg:grid-cols-6">
{props.icons.map((icon) => {
return (
<div key={icon} className="flex items-center gap-1">
<Icon name={icon} className={props.iconClassName} />
<div>{icon}</div>
</div>
);
})}
</div>
</div>
);
+46
View File
@@ -0,0 +1,46 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import { type IconName, IconSprites } from "@calcom/ui";
import { lucideIconList } from "../../../../packages/ui/components/icon/icon-list.mjs";
import { IconGrid } from "./IconGrid";
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "swap",
weight: "600",
});
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("icon_showcase"),
() => ""
);
};
export default async function IconsPage() {
const icons = Array.from(lucideIconList).sort() as IconName[];
const t = await getTranslate();
return (
<div className={`${interFont.variable} ${calFont.variable}`}>
<div className="bg-subtle flex h-screen">
<IconSprites />
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
<h1 className="text-emphasis font-cal text-2xl font-medium">{t("icons_showcase")}</h1>
<IconGrid title="Regular Icons" icons={icons} />
<IconGrid
title="Filled Icons"
icons={icons}
rootClassName="bg-darkgray-100 text-gray-50"
iconClassName="fill-blue-500"
/>
</div>
</div>
</div>
);
}
export const dynamic = "force-static";
+5 -1
View File
@@ -26,7 +26,10 @@ export const generateMetadata = () => prepareRootMetadata();
const getInitialProps = async (url: string) => {
const { pathname, searchParams } = new URL(url);
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
const isEmbedSnippetGeneratorPath = pathname.startsWith("/event-types");
const isEmbed =
(pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null) &&
!isEmbedSnippetGeneratorPath;
const embedColorScheme = searchParams?.get("ui.color-scheme");
const req = { headers: headers(), cookies: cookies() };
@@ -76,6 +79,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<script
nonce={nonce}
id="headScript"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `
window.calNewLocale = "${locale}";
@@ -0,0 +1,54 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
const rescheduleUid = booking?.uid;
const profileName = eventData?.profile?.name ?? "";
const profileImage = eventData?.profile.image ?? "";
return await generateEventBookingPageMetadata({
profile: { name: profileName, image: profileImage },
event: {
title: eventData?.title ?? "",
hidden: eventData?.hidden ?? false,
users: [
...(eventData?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable: !!isSEOIndexable,
isReschedule: !!rescheduleUid,
});
};
export const Page = async (props: OrgTypePageProps) => {
if ((props as TeamTypePageProps)?.teamId) {
return <TeamTypePage {...(props as TeamTypePageProps)} />;
}
return <UserTypePage {...(props as UserTypePageProps)} />;
};
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
@@ -0,0 +1,64 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import {
generateTeamProfilePageMetadata,
generateUserProfilePageMetadata,
} from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export type OrgPageProps = UserPageProps | TeamPageProps;
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx);
if ((props as TeamPageProps)?.team) {
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = props as TeamPageProps;
return await generateTeamProfilePageMetadata({
profile: {
teamName: team.name,
image: getOrgOrTeamAvatar(team),
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: currentOrgDomain ?? null,
isSEOIndexable: !!isSEOIndexable,
});
} else {
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props as UserPageProps;
const isOrg = !!profile?.organization;
const allowSEOIndexing =
(!isOrg && profile.allowSEOIndexing) || (isOrg && isOrgSEOIndexable && profile.allowSEOIndexing);
return await generateUserProfilePageMetadata({
profile: { name: profile.name, username: profile.username, image: profile.image, markdownStrippedBio },
hideBranding: false,
orgSlug: entity.orgSlug ?? null,
isSEOIndexable: !!allowSEOIndexing,
event: null,
});
}
};
export const Page = async (props: OrgPageProps) => {
if ((props as TeamPageProps)?.team) {
return <TeamPage {...(props as TeamPageProps)} />;
}
return <UserPage {...(props as UserPageProps)} />;
};
export default WithLayout({ getLayout: null, getData, ServerPage: Page, isBookingPage: true });
@@ -4,7 +4,6 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers, cookies } from "next/headers";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventRepository } from "@calcom/lib/server/repository/event";
@@ -16,7 +15,7 @@ import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-mee
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { slug: eventSlug, user: username } = await getData(context);
const { slug: eventSlug, user: username, isBrandingHidden } = await getData(context);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -34,9 +33,10 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
return await _generateMetadata(
() => `${title} | ${profileName}`,
() => `${title}`
() => `${title}`,
isBrandingHidden
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout, getData, Page })<"P">;
export default WithLayout({ getData, Page, isBookingPage: true })<"P">;
+1
View File
@@ -0,0 +1 @@
export { default, generateMetadata } from "app/team/[slug]/page";
@@ -0,0 +1 @@
export { default, generateMetadata } from "app/team/[slug]/[type]/page";
@@ -0,0 +1 @@
export { default, generateMetadata } from "app/team/[slug]/page";
@@ -1,6 +1,6 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { generateEventBookingPageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
@@ -15,7 +15,7 @@ import LegacyPage, { type PageProps as LegacyPageProps } from "~/team/type-view"
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;
const { user: username, slug: eventSlug, booking, isSEOIndexable, eventData, isBrandingHidden } = props;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req, legacyCtx.params?.orgSlug);
const event = await EventRepository.getPublicEvent({
@@ -26,13 +26,26 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
);
return await generateEventBookingPageMetadata({
profile: {
name: event?.profile?.name ?? "",
image: event?.profile.image ?? "",
},
event: {
title: event?.title ?? "",
hidden: event?.hidden ?? false,
users: [
...(event?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
},
hideBranding: isBrandingHidden,
orgSlug: eventData?.entity.orgSlug ?? null,
isSEOIndexable,
isReschedule: !!booking,
});
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
@@ -1,9 +1,11 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { generateTeamProfilePageMetadata } from "app/generateBookingPageMetadata";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
@@ -11,12 +13,21 @@ import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
return await _generateMetadata(
(t) => props.team.name || t("nameless_team"),
(t) => props.team.name || t("nameless_team")
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams)
);
return await generateTeamProfilePageMetadata({
profile: {
teamName: team.name,
image: getOrgOrTeamAvatar(team),
markdownStrippedBio: markdownStrippedBio,
},
event: null,
hideBranding: false,
orgSlug: currentOrgDomain ?? null,
isSEOIndexable: !!isSEOIndexable,
});
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
@@ -25,4 +36,5 @@ export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -11,7 +11,7 @@ import "@calcom/dayjs/locales";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import { useCopy } from "@calcom/lib/hooks/useCopy";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
@@ -1,3 +1,4 @@
import type { EmbedProps } from "app/WithEmbedSSR";
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
@@ -12,7 +13,6 @@ import { RedirectType } from "@calcom/prisma/enums";
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "app/WithEmbedSSR";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
@@ -1,4 +1,3 @@
import { getServerSideProps as GSSUserPage } from "@pages/[user]";
import type { GetServerSidePropsContext } from "next";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
@@ -6,6 +5,8 @@ import prisma from "@calcom/prisma";
import { getServerSideProps as GSSTeamPage } from "@lib/team/[slug]/getServerSideProps";
import { getServerSideProps as GSSUserPage } from "@server/lib/[user]/getServerSideProps";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({
where: {
@@ -55,7 +55,6 @@ const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefine
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug);
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
context.req,
context.params?.orgSlug ?? context.query?.orgSlug
+6 -1
View File
@@ -153,6 +153,8 @@ export const config = {
// Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here.
// https://github.com/vercel/next.js/discussions/42458
matcher: [
"/403",
"/500",
"/d/:path*",
"/more/:path*",
"/maintenance/:path*",
@@ -193,7 +195,10 @@ export const config = {
"/settings/:path*",
"/reschedule/:path*",
"/availability/:path*",
"/booking/:path*",
"/org/:path*",
"/team/:path*",
"/:user/:type/",
"/:user/",
],
};
@@ -827,7 +827,8 @@ export default function Success(props: PageProps) {
)}`
: ""
}`}
className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2">
className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2"
target="_blank">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
@@ -2,7 +2,6 @@
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";
@@ -20,13 +19,6 @@ export default function Type({
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
entity={entity}
/>
<Booker
username={user}
eventSlug={slug}
@@ -2,7 +2,6 @@
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 { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -13,15 +12,6 @@ export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
function Type({ slug, user, booking, isEmbed, isBrandingHidden, entity, eventTypeId, duration }: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={undefined}
hideBranding={isBrandingHidden}
isTeamEvent
entity={entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
+78 -97
View File
@@ -12,7 +12,6 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -20,7 +19,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
@@ -164,105 +163,87 @@ function TeamPage({
const profileImageSrc = getOrgOrTeamAvatar(team);
return (
<>
<HeadSeo
origin={getOrgFullOrigin(currentOrgDomain)}
title={teamName}
description={teamName}
meeting={{
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: profileImageSrc,
},
}}
nextSeoProps={{
nofollow: !isSEOIndexable,
noindex: !isSEOIndexable,
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
)}
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
<div className="relative">
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
</div>
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
{!isBioEmpty && (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
)}
</main>
</>
</div>
{team.isOrganization ? (
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
</h2>
</div>
) : (
<Team members={team.members} teamName={team.name} />
))}
{!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes eventTypes={team.eventTypes} />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
</div>
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon="arrow-right"
data-testid="book-a-team-member-btn"
className="dark:hover:bg-darkgray-200"
href={{
pathname: `${isValidOrgDomain ? "" : "/team"}/${team.slug}`,
query: {
...queryParamsToForward,
members: "1",
},
}}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div>
)}
</>
)}
</main>
);
}
+1 -24
View File
@@ -1,14 +1,13 @@
"use client";
import type { EmbedProps } from "app/WithEmbedSSR";
import { useSearchParams } from "next/navigation";
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 { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "app/WithEmbedSSR";
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
@@ -34,33 +33,11 @@ function Type({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isSEOIndexable,
}: PageProps) {
const searchParams = useSearchParams();
const { profile, users, hidden, title } = eventData;
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
eventData={
profile && users && title && hidden !== undefined
? {
profile,
users,
title,
hidden,
}
: undefined
}
entity={eventData.entity}
bookingData={booking}
isSEOIndexable={isSEOIndexable}
/>
<Booker
username={user}
eventSlug={slug}
@@ -1,9 +1,8 @@
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { describe, it, vi } from "vitest";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { HeadSeo } from "@calcom/ui";
import UserPage from "./users-public-view";
@@ -71,33 +70,5 @@ describe("UserPage Component", () => {
});
render(<UserPage {...mockData.props} />);
const expectedDescription = mockData.props.markdownStrippedBio;
const expectedTitle = expectedDescription;
expect(HeadSeo).toHaveBeenCalledWith(
{
origin: `${mockData.props.entity.orgSlug}.cal.local`,
title: `${mockData.props.profile.name}`,
description: expectedDescription,
meeting: {
profile: {
name: mockData.props.profile.name,
image: mockData.props.users[0].avatarUrl,
},
title: expectedTitle,
users: [
{
name: mockData.props.users[0].name,
username: mockData.props.users[0].username,
},
],
},
nextSeoProps: {
nofollow: !mockData.props.profile.allowSEOIndexing,
noindex: !mockData.props.profile.allowSEOIndexing,
},
},
{}
);
});
});
@@ -11,12 +11,11 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { HeadSeo, Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import { Icon, UnpublishedEntity, UserAvatar } from "@calcom/ui";
import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
@@ -60,112 +59,89 @@ export function UserPage(props: PageProps) {
const isEventListEmpty = eventTypes.length === 0;
const isOrg = !!user?.profile?.organization;
const allowSEOIndexing = isOrg
? isOrgSEOIndexable
? profile.allowSEOIndexing
: false
: profile.allowSEOIndexing;
return (
<>
<HeadSeo
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
title={profile.name}
description={markdownStrippedBio}
meeting={{
title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: user.avatarUrl || null },
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !allowSEOIndexing,
nofollow: !allowSEOIndexing,
}}
/>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "border-booker border-booker-width bg-default rounded-md" : "",
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<UserAvatar
size="xl"
user={{
avatarUrl: user.avatarUrl,
profile: user.profile,
name: profile.name,
username: profile.username,
}}
/>
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
{profile.name}
{!isOrg && user.verified && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black"
/>
)}
</div>
{isOrg && (
<Icon
name="badge-check"
className="mx-1 -mt-1 inline h-6 w-6 fill-yellow-500 text-white dark:text-black"
/>
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
<div
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{eventTypes.map((type) => (
<Link
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
prefetch={false}
href={{
pathname: `/${user.profile.username}/${type.slug}`,
query,
}}
passHref
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b transition first:rounded-t-md last:rounded-b-md last:border-b-0"
data-testid="event-type-link">
<Icon
name="arrow-right"
className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<div className="flex flex-wrap items-center">
<h2 className="text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
</Link>
))}
</div>
<EventTypeDescription eventType={type} isPublic={true} shortenDescription />
</div>
</Link>
))}
</div>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
</>
{isEventListEmpty && <EmptyPage name={profile.name || "User"} />}
</main>
<Toaster position="bottom-right" />
</div>
);
}
UserPage.isBookingPage = true;
@@ -1,13 +1,12 @@
"use client";
import type { EmbedProps } from "app/WithEmbedSSR";
import { useSearchParams } from "next/navigation";
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 { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "app/WithEmbedSSR";
import type { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
@@ -23,40 +22,11 @@ export const getMultipleDurationValue = (
return defaultValue;
};
function Type({
slug,
user,
isEmbed,
booking,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
eventData,
orgBannerUrl,
}: PageProps) {
function Type({ slug, user, isEmbed, booking, isBrandingHidden, eventData, orgBannerUrl }: PageProps) {
const searchParams = useSearchParams();
const { profile, users, hidden, title } = eventData;
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
eventData={
profile && users && title && hidden !== undefined
? {
profile,
users,
title,
hidden,
}
: undefined
}
entity={eventData.entity}
bookingData={booking}
/>
<Booker
username={user}
eventSlug={slug}
+6 -1
View File
@@ -395,6 +395,11 @@ const nextConfig = {
value: "cross-origin",
};
const ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = {
key: "Access-Control-Allow-Origin",
value: "*",
};
return [
{
source: "/auth/:path*",
@@ -464,7 +469,7 @@ const nextConfig = {
},
{
source: "/icons/sprite.svg",
headers: [CORP_CROSS_ORIGIN_HEADER],
headers: [CORP_CROSS_ORIGIN_HEADER, ACCESS_CONTROL_ALLOW_ORIGIN_HEADER],
},
],
...(isOrganizationsEnabled
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.8.11",
"version": "4.8.13",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
-54
View File
@@ -1,54 +0,0 @@
import Head from "next/head";
import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
export default function Error500() {
const searchParams = useCompatSearchParams();
const { t } = useLocale();
return (
<div className="bg-subtle flex h-screen">
<Head>
<title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">It&apos;s not you, it&apos;s us.</h2>
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">{t("something_went_wrong_on_our_end")}</p>
{searchParams?.get("error") && (
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
{t("please_provide_following_text_to_suppport")}:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{searchParams?.get("error")}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon="copy"
onClick={() => {
navigator.clipboard.writeText(searchParams?.get("error") as string);
showToast("Link copied!", "success");
}}>
{t("copy")}
</Button>
</pre>
</div>
)}
<Button href="mailto:support@cal.com">{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
{t("go_back")}
</Button>
</div>
</div>
);
}
Error500.PageWrapper = PageWrapper;
+10 -1
View File
@@ -1,7 +1,16 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
import type { PageProps } from "~/users/views/users-type-public-view";
import TypePage from "~/users/views/users-type-public-view";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
export { default } from "./index";
const Type = (props: PageProps) => <TypePage {...props} />;
Type.PageWrapper = PageWrapper;
export default Type;
-12
View File
@@ -1,12 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/users/views/users-type-public-view";
import TypePage from "~/users/views/users-type-public-view";
export { getServerSideProps } from "@server/lib/[user]/[type]/getServerSideProps";
const Type = (props: PageProps) => <TypePage {...props} />;
Type.PageWrapper = PageWrapper;
export default Type;
+17 -4
View File
@@ -1,6 +1,19 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { getServerSideProps as _getServerSideProps } from "@server/lib/[user]/getServerSideProps";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export { default } from "./index";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
export type PageProps = UserPageProps | TeamPageProps;
function Page(props: PageProps) {
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
}
Page.PageWrapper = PageWrapper;
export default Page;
-11
View File
@@ -1,11 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import User, { type PageProps } from "~/users/views/users-public-view";
export { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
const UserPage = (props: PageProps) => <User {...props} />;
UserPage.PageWrapper = PageWrapper;
export default UserPage;
-77
View File
@@ -1,77 +0,0 @@
"use client";
import type { InferGetStaticPropsType } from "next";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import Head from "next/head";
import { APP_NAME } from "@calcom/lib/constants";
import type { IconName } from "@calcom/ui";
import { Icon, IconSprites } from "@calcom/ui";
import { lucideIconList } from "../../../packages/ui/components/icon/icon-list.mjs";
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "swap",
weight: "600",
});
export const getStaticProps = async () => {
return {
props: {
icons: Array.from(lucideIconList).sort() as IconName[],
},
};
};
const IconGrid = (props: {
title: string;
icons: IconName[];
rootClassName?: string;
iconClassName?: string;
}) => (
<div className={props.rootClassName}>
<h2 className="font-cal mt-6 text-lg font-medium">{props.title}</h2>
<div className="grid grid-cols-2 lg:grid-cols-6">
{props.icons.map((icon) => {
return (
<div key={icon} className="flex items-center gap-1">
<Icon name={icon} className={props.iconClassName} />
<div>{icon}</div>
</div>
);
})}
</div>
</div>
);
export default function IconsPage(props: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div className="bg-subtle flex h-screen">
<Head>
<title>Icon showcase | {APP_NAME}</title>
</Head>
<style jsx global>{`
:root {
--font-cal: ${calFont.style.fontFamily};
--font-inter: ${interFont.style.fontFamily};
}
`}</style>
<IconSprites />
<div className="bg-default m-auto min-w-full rounded-md p-10 text-right ltr:text-left">
<h1 className="text-emphasis font-cal text-2xl font-medium">Icons showcase</h1>
<IconGrid title="Regular Icons" icons={props.icons} />
<IconGrid
title="Filled Icons"
icons={props.icons}
rootClassName="bg-darkgray-100 text-gray-50"
iconClassName="fill-blue-500"
/>
</div>
</div>
);
}
@@ -1,7 +1,20 @@
import { getServerSideProps as _getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from ".";
import PageWrapper from "@components/PageWrapper";
export { default, type PageProps } from ".";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export type PageProps = UserTypePageProps | TeamTypePageProps;
export default function Page(props: PageProps) {
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
}
Page.PageWrapper = PageWrapper;
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
@@ -1,17 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamTypePageProps } from "~/team/type-view";
import TeamTypePage from "~/team/type-view";
import UserTypePage from "~/users/views/users-type-public-view";
import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
export type PageProps = UserTypePageProps | TeamTypePageProps;
export default function Page(props: PageProps) {
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
return <UserTypePage {...(props as UserTypePageProps)} />;
}
Page.PageWrapper = PageWrapper;
@@ -1,9 +1 @@
"use client";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[user]";
export { default } from "../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
export { default, getServerSideProps } from "@pages/[user]/embed";
@@ -1,21 +0,0 @@
"use client";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as TeamPageProps } from "~/team/team-view";
import TeamPage from "~/team/team-view";
import UserPage from "~/users/views/users-public-view";
import type { PageProps as UserPageProps } from "~/users/views/users-public-view";
export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
export type PageProps = UserPageProps | TeamPageProps;
function Page(props: PageProps) {
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
return <UserPage {...(props as UserPageProps)} />;
}
Page.PageWrapper = PageWrapper;
export default Page;
+9 -2
View File
@@ -1,7 +1,14 @@
import { getServerSideProps as _getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "./index";
import PageWrapper from "@components/PageWrapper";
export { default } from "./index";
import TeamPage, { type PageProps } from "~/team/team-view";
const Page = (props: PageProps) => <TeamPage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
-1
View File
@@ -1 +0,0 @@
export { getServerSideProps, default } from "@pages/team/[slug]";
@@ -1,10 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
import Type from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
const Page = (props: PageProps) => <Type {...props} />;
Page.PageWrapper = PageWrapper;
export { getServerSideProps } from "@lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps";
export default Page;
@@ -1 +0,0 @@
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
@@ -1 +0,0 @@
export { getServerSideProps, default } from "@pages/team/[slug]";
-12
View File
@@ -1,12 +0,0 @@
import { getServerSideProps } from "@lib/signup/getServerSideProps";
import PageWrapper from "@components/PageWrapper";
import type { SignupProps } from "~/signup-view";
import Signup from "~/signup-view";
const Page = (props: SignupProps) => <Signup {...props} />;
export { getServerSideProps };
Page.PageWrapper = PageWrapper;
export default Page;
-10
View File
@@ -1,10 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import TeamPage, { type PageProps } from "~/team/team-view";
const Page = (props: PageProps) => <TeamPage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
-10
View File
@@ -1,10 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import TypePage, { type PageProps } from "~/team/type-view";
const Page = (props: PageProps) => <TypePage {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
@@ -11,6 +11,8 @@ import {
selectSecondAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test("dynamic booking", async ({ page, users }) => {
@@ -66,7 +66,7 @@ async function verifyRobotsMetaTag({ page, orgSlug, urls, expectedContent }: Ver
await doOnOrgDomain({ orgSlug, page }, async ({ page, goToUrlWithErrorHandling }) => {
for (const relativeUrl of urls) {
const { url } = await goToUrlWithErrorHandling(relativeUrl);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTag = await page.locator('head > meta[name="robots"]').first();
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual(expectedContent);
}
@@ -103,7 +103,7 @@ test.describe("Organization Settings", () => {
`/${orgMember.username}`,
`/${orgMember.username}/${userEvent.slug}`,
],
expectedContent: "noindex,nofollow",
expectedContent: "noindex, nofollow",
});
});
});
@@ -130,7 +130,7 @@ test.describe("Organization Settings", () => {
`/${orgMember.username}`,
`/${orgMember.username}/${userEvent.slug}`,
],
expectedContent: "index,follow",
expectedContent: "index, follow",
});
});
});
@@ -148,7 +148,7 @@ test.describe("Organization Settings", () => {
page,
orgSlug: org.slug,
urls: [`/${orgMember.username}/${userEvent.slug}`],
expectedContent: "noindex,nofollow",
expectedContent: "noindex, nofollow",
});
});
});
@@ -2031,8 +2031,12 @@
"event_end_time_info": "وقت نهاية الحدث",
"cancel_url_variable": "رابط الإلغاء",
"cancel_url_info": "رابط إلغاء الحجز",
"cancel_reason_variable": "سبب الإلغاء",
"cancel_reason_info": "سبب إلغاء الحجز",
"reschedule_url_variable": "رابط إعادة الجدولة",
"reschedule_url_info": "رابط لإعادة جدولة الحجز",
"reschedule_reason_variable": "سبب إعادة الجدولة",
"reschedule_reason_info": "سبب إعادة جدولة الحجز",
"invalid_event_name_variables": "هناك متغيّر غير صالح في اسم الحدث",
"select_all": "تحديد الكل",
"default_conferencing_bulk_title": "التحديث الجماعي لمجموعة أنواع الأحداث الحالية",
@@ -2737,6 +2741,12 @@
"month_to_date": "منذ بداية الشهر",
"year_to_date": "منذ بداية السنة",
"custom_range": "نطاق مخصص",
"refund_policy": "سياسة الاسترداد",
"always": "دائماً",
"never": "أبداً",
"payment_option": "خيار الدفع",
"if_cancelled": "في حالة الإلغاء",
"before": "قبل",
"show_all_columns": "عرض جميع الأعمدة",
"toggle_columns": "تبديل الأعمدة",
"no_columns_found": "لم يتم العثور على أعمدة",
@@ -2899,5 +2909,13 @@
"calibration": "المعايرة",
"shortfall": "العجز",
"routing_form": "نموذج التوجيه",
"go_back_and_save": "العودة والحفظ",
"leave_without_saving": "المغادرة دون حفظ",
"leave_without_saving_description": "هل أنت متأكد من أنك تريد المغادرة دون حفظ التغييرات في نموذج التوجيه الخاص بك؟",
"something_unexpected_occurred": "حدث خطأ غير متوقع",
"500_error_message": "الخطأ من جانبنا، وليس من جانبك.",
"dry_run_mode_active": "أنت تقوم بحجز تجريبي.",
"icon_showcase": "معرض الأيقونات",
"icons_showcase": "معرض الأيقونات",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Tədbirin bitmə vaxtı",
"cancel_url_variable": "Ləğv URL",
"cancel_url_info": "Rezervasiyanı ləğv etmək üçün URL",
"cancel_reason_variable": "Ləğv etmə səbəbi",
"cancel_reason_info": "Rezervasiyanın ləğv edilməsi səbəbi",
"reschedule_url_variable": "Yenidən planlaşdırma URL",
"reschedule_url_info": "Rezervasiyanı yenidən planlaşdırmaq üçün URL",
"reschedule_reason_variable": "Yenidən planlaşdırma səbəbi",
"reschedule_reason_info": "Rezervasiyanın yenidən planlaşdırılması səbəbi",
"invalid_event_name_variables": "{{item}} tədbir adında etibarsız dəyişəndir",
"select_all": "Hamısını seç",
"default_conferencing_bulk_title": "Mövcud tədbir növlərini toplu yeniləmə",
@@ -2737,6 +2741,12 @@
"month_to_date": "bu ayın tarixinə qədər",
"year_to_date": "bu ilin tarixinə qədər",
"custom_range": "xüsusi aralıq",
"refund_policy": "Geri ödəmə siyasəti",
"always": "Həmişə",
"never": "Heç vaxt",
"payment_option": "Ödəniş seçimi",
"if_cancelled": "Ləğv edilərsə",
"before": "əvvəl",
"show_all_columns": "Bütün sütunları göstər",
"toggle_columns": "Sütunları dəyiş",
"no_columns_found": "Sütun tapılmadı",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrasiya",
"shortfall": "Çatışmazlıq",
"routing_form": "Marşrutlama forması",
"go_back_and_save": "Geri qayıt və yadda saxla",
"leave_without_saving": "Yadda saxlamadan çıx",
"leave_without_saving_description": "Marşrutlaşdırma formanızdakı dəyişiklikləri yadda saxlamadan çıxmaq istədiyinizə əminsiniz?",
"something_unexpected_occurred": "Gözlənilməz bir xəta baş verdi",
"500_error_message": "Problem sizdə deyil, bizdədir.",
"dry_run_mode_active": "Siz test rezervasiyası edirsiniz.",
"icon_showcase": "İkon nümayişi",
"icons_showcase": "İkonların nümayişi",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Крайният час на събитието",
"cancel_url_variable": "URL за отказ",
"cancel_url_info": "URL адресът за отказване на резервацията",
"cancel_reason_variable": "Причина за отказ",
"cancel_reason_info": "Причината за отказване на резервацията",
"reschedule_url_variable": "URL за препланиране",
"reschedule_url_info": "URL адресът за препланиране на резервацията",
"reschedule_reason_variable": "Причина за препланиране",
"reschedule_reason_info": "Причината за препланиране на резервацията",
"invalid_event_name_variables": "{{item}} е невалидна променлива в името на събитието",
"select_all": "Избери всички",
"default_conferencing_bulk_title": "Масова актуализация на съществуващите типове събития",
@@ -2737,6 +2741,12 @@
"month_to_date": "от началото на месеца",
"year_to_date": "от началото на годината",
"custom_range": "персонализиран период",
"refund_policy": "Политика за възстановяване",
"always": "Винаги",
"never": "Никога",
"payment_option": "Опция за плащане",
"if_cancelled": "При отказ",
"before": "преди",
"show_all_columns": "Покажи всички колони",
"toggle_columns": "Превключи колоните",
"no_columns_found": "Няма намерени колони",
@@ -2899,5 +2909,13 @@
"calibration": "Калибриране",
"shortfall": "Недостиг",
"routing_form": "Форма за маршрутизиране",
"go_back_and_save": "Върни се и запази",
"leave_without_saving": "Излез без да запазиш",
"leave_without_saving_description": "Сигурни ли сте, че искате да излезете без да запазите промените във вашата форма за маршрутизиране?",
"something_unexpected_occurred": "Възникна неочаквана грешка",
"500_error_message": "Не сте вие, ние сме.",
"dry_run_mode_active": "Правите тестова резервация.",
"icon_showcase": "Галерия с икони",
"icons_showcase": "Галерия с икони",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "L'hora de finalització de l'esdeveniment",
"cancel_url_variable": "URL de cancel·lació",
"cancel_url_info": "L'URL per cancel·lar la reserva",
"cancel_reason_variable": "Motiu de cancel·lació",
"cancel_reason_info": "El motiu per cancel·lar la reserva",
"reschedule_url_variable": "URL de reprogramació",
"reschedule_url_info": "L'URL per reprogramar la reserva",
"reschedule_reason_variable": "Motiu de reprogramació",
"reschedule_reason_info": "El motiu per reprogramar la reserva",
"invalid_event_name_variables": "{{item}} és una variable no vàlida en el nom del teu esdeveniment",
"select_all": "Selecciona-ho tot",
"default_conferencing_bulk_title": "Actualització massiva dels tipus d'esdeveniments existents",
@@ -2737,6 +2741,12 @@
"month_to_date": "mes fins avui",
"year_to_date": "any fins avui",
"custom_range": "rang personalitzat",
"refund_policy": "Política de reemborsament",
"always": "Sempre",
"never": "Mai",
"payment_option": "Opció de pagament",
"if_cancelled": "Si es cancel·la",
"before": "abans",
"show_all_columns": "Mostra totes les columnes",
"toggle_columns": "Commuta columnes",
"no_columns_found": "No s'han trobat columnes",
@@ -2899,5 +2909,13 @@
"calibration": "Calibratge",
"shortfall": "Dèficit",
"routing_form": "Formulari d'encaminament",
"go_back_and_save": "Torna enrere i desa",
"leave_without_saving": "Surt sense desar",
"leave_without_saving_description": "Estàs segur que vols sortir sense desar els canvis al formulari de rutes?",
"something_unexpected_occurred": "S'ha produït un error inesperat",
"500_error_message": "No ets tu, som nosaltres.",
"dry_run_mode_active": "Estàs fent una reserva de prova.",
"icon_showcase": "Mostra d'icones",
"icons_showcase": "Mostra d'icones",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Čas ukončení události",
"cancel_url_variable": "Adresa URL pro zrušení",
"cancel_url_info": "Adresa URL pro zrušení rezervace",
"cancel_reason_variable": "Důvod zrušení",
"cancel_reason_info": "Důvod pro zrušení rezervace",
"reschedule_url_variable": "Adresa URL pro změnu termínu",
"reschedule_url_info": "Adresa URL pro změnu termínu rezervace",
"reschedule_reason_variable": "Důvod přeplánování",
"reschedule_reason_info": "Důvod pro přeplánování rezervace",
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
"select_all": "Vybrat vše",
"default_conferencing_bulk_title": "Hromadná aktualizace stávajících typů událostí",
@@ -2737,6 +2741,12 @@
"month_to_date": "od začátku měsíce",
"year_to_date": "od začátku roku",
"custom_range": "vlastní rozsah",
"refund_policy": "Pravidla pro vrácení peněz",
"always": "Vždy",
"never": "Nikdy",
"payment_option": "Možnost platby",
"if_cancelled": "Při zrušení",
"before": "před",
"show_all_columns": "Zobrazit všechny sloupce",
"toggle_columns": "Přepnout sloupce",
"no_columns_found": "Žádné sloupce nebyly nalezeny",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrace",
"shortfall": "Nedostatek",
"routing_form": "Směrovací formulář",
"go_back_and_save": "Vrátit se a uložit",
"leave_without_saving": "Odejít bez uložení",
"leave_without_saving_description": "Opravdu chcete odejít bez uložení změn ve vašem směrovacím formuláři?",
"something_unexpected_occurred": "Došlo k neočekávané chybě",
"500_error_message": "Není to vámi, je to námi.",
"dry_run_mode_active": "Provádíte testovací rezervaci.",
"icon_showcase": "Ukázka ikon",
"icons_showcase": "Ukázka ikon",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Sluttidspunkt for begivenheden",
"cancel_url_variable": "Annuller URL",
"cancel_url_info": "URL'en til at annullere bookingen",
"cancel_reason_variable": "Aflysningsårsag",
"cancel_reason_info": "Årsagen til at aflyse bookingen",
"reschedule_url_variable": "Omlæg URL",
"reschedule_url_info": "URL'en til at omlægge bookingen",
"reschedule_reason_variable": "Omlægningsårsag",
"reschedule_reason_info": "Årsagen til at omlægge bookingen",
"invalid_event_name_variables": "{{item}} er en ugyldig variabel i dit begivenhedsnavn",
"select_all": "Vælg alle",
"default_conferencing_bulk_title": "Masseopdater eksisterende begivenhedstyper",
@@ -2737,6 +2741,12 @@
"month_to_date": "måned til dato",
"year_to_date": "år til dato",
"custom_range": "tilpasset interval",
"refund_policy": "Refundpolitik",
"always": "Altid",
"never": "Aldrig",
"payment_option": "Betalingsmulighed",
"if_cancelled": "Hvis annulleret",
"before": "før",
"show_all_columns": "Vis alle kolonner",
"toggle_columns": "Skift kolonner",
"no_columns_found": "Ingen kolonner fundet",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrering",
"shortfall": "Underskud",
"routing_form": "Routing-formular",
"go_back_and_save": "Gå tilbage og gem",
"leave_without_saving": "Forlad uden at gemme",
"leave_without_saving_description": "Er du sikker på, at du vil forlade siden uden at gemme ændringerne i din rutningsformular?",
"something_unexpected_occurred": "Der opstod en uventet fejl",
"500_error_message": "Det er ikke dig, det er os.",
"dry_run_mode_active": "Du foretager en testbooking.",
"icon_showcase": "Ikonoversigt",
"icons_showcase": "Ikonoversigt",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Der Endzeitpunkt des Termins",
"cancel_url_variable": "Absage-URL",
"cancel_url_info": "Die URL, um die Buchung abzusagen",
"cancel_reason_variable": "Stornierungsgrund",
"cancel_reason_info": "Der Grund für die Stornierung der Buchung",
"reschedule_url_variable": "Umplanungs-URL",
"reschedule_url_info": "Die URL, um die Buchung umzuplanen",
"reschedule_reason_variable": "Umbuchungsgrund",
"reschedule_reason_info": "Der Grund für die Umbuchung des Termins",
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
"select_all": "Alle auswählen",
"default_conferencing_bulk_title": "Massenaktualisierung vorhandener Termintypen",
@@ -2737,6 +2741,12 @@
"month_to_date": "aktueller Monat",
"year_to_date": "aktuelles Jahr",
"custom_range": "benutzerdefinierter Zeitraum",
"refund_policy": "Rückerstattungsrichtlinie",
"always": "Immer",
"never": "Nie",
"payment_option": "Zahlungsoption",
"if_cancelled": "Bei Stornierung",
"before": "vor",
"show_all_columns": "Alle Spalten anzeigen",
"toggle_columns": "Spalten ein-/ausblenden",
"no_columns_found": "Keine Spalten gefunden",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrierung",
"shortfall": "Fehlmenge",
"routing_form": "Routing-Formular",
"go_back_and_save": "Zurück und speichern",
"leave_without_saving": "Ohne speichern verlassen",
"leave_without_saving_description": "Sind Sie sicher, dass Sie die Änderungen an Ihrem Routing-Formular nicht speichern möchten?",
"something_unexpected_occurred": "Ein unerwarteter Fehler ist aufgetreten",
"500_error_message": "Es liegt nicht an Ihnen, sondern an uns.",
"dry_run_mode_active": "Sie führen eine Testbuchung durch.",
"icon_showcase": "Icon-Übersicht",
"icons_showcase": "Icons-Übersicht",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Η ώρα λήξης του συμβάντος",
"cancel_url_variable": "URL ακύρωσης",
"cancel_url_info": "Το URL για την ακύρωση της κράτησης",
"cancel_reason_variable": "Αιτία ακύρωσης",
"cancel_reason_info": "Ο λόγος για την ακύρωση της κράτησης",
"reschedule_url_variable": "URL επαναπρογραμματισμού",
"reschedule_url_info": "Το URL για τον επαναπρογραμματισμό της κράτησης",
"reschedule_reason_variable": "Αιτία αλλαγής",
"reschedule_reason_info": "Ο λόγος για την αλλαγή της κράτησης",
"invalid_event_name_variables": "Το {{item}} είναι μια μη έγκυρη μεταβλητή στο όνομα του συμβάντος σας",
"select_all": "Επιλογή όλων",
"default_conferencing_bulk_title": "Μαζική ενημέρωση υπαρχόντων τύπων συμβάντων",
@@ -2737,6 +2741,12 @@
"month_to_date": "από την αρχή του μήνα",
"year_to_date": "από την αρχή του έτους",
"custom_range": "προσαρμοσμένο εύρος",
"refund_policy": "Πολιτική επιστροφής χρημάτων",
"always": "Πάντα",
"never": "Ποτέ",
"payment_option": "Επιλογή πληρωμής",
"if_cancelled": "Εάν ακυρωθεί",
"before": "πριν",
"show_all_columns": "Εμφάνιση όλων των στηλών",
"toggle_columns": "Εναλλαγή στηλών",
"no_columns_found": "Δεν βρέθηκαν στήλες",
@@ -2899,5 +2909,13 @@
"calibration": "Βαθμονόμηση",
"shortfall": "Έλλειμμα",
"routing_form": "Φόρμα δρομολόγησης",
"go_back_and_save": "Επιστροφή και αποθήκευση",
"leave_without_saving": "Έξοδος χωρίς αποθήκευση",
"leave_without_saving_description": "Είστε βέβαιοι ότι θέλετε να αποχωρήσετε χωρίς να αποθηκεύσετε τις αλλαγές στη φόρμα δρομολόγησης;",
"something_unexpected_occurred": "Συνέβη κάτι απρόσμενο",
"500_error_message": "Δεν φταίτε εσείς, εμείς φταίμε.",
"dry_run_mode_active": "Κάνετε μια δοκιμαστική κράτηση.",
"icon_showcase": "Παρουσίαση εικονιδίων",
"icons_showcase": "Παρουσίαση εικονιδίων",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+25 -11
View File
@@ -2031,8 +2031,12 @@
"event_end_time_info": "The event end time",
"cancel_url_variable": "Cancel URL",
"cancel_url_info": "The URL to cancel the booking",
"cancel_reason_variable": "Cancellation Reason",
"cancel_reason_info": "The reason for canceling the booking",
"reschedule_url_variable": "Reschedule URL",
"reschedule_url_info": "The URL to reschedule the booking",
"reschedule_reason_variable": "Reschedule Reason",
"reschedule_reason_info": "The reason for rescheduling the booking",
"invalid_event_name_variables": "{{item}} is an invalid variable in your event name",
"select_all": "Select All",
"default_conferencing_bulk_title": "Bulk update existing event types",
@@ -2528,26 +2532,22 @@
"schedule_timezone_change": "Schedule timezone change",
"date": "Date",
"overlaps_with_existing_schedule": "This overlaps with an existing schedule. Please select a different date.",
"org_admin_no_slots|subject": "No availability found for {{name}}",
"org_admin_no_slots|heading": "No availability found for {{name}}",
"org_admin_no_slots|content": "Hello Organization Admins,<br /><br />Please note: It has been brought to our attention that {{username}} has not had any availability when a user has visited {{username}}/{{slug}}<br /><br />There’s a few reasons why this could be happening<br />The user does not have any calendars connected<br />Their schedules attached to this event are not enabled<br /> <br />We recommend checking their availability to resolve this.",
"org_admin_no_slots|cta": "Open users availability",
"organization_no_slots_notification_switch_title": "Get notifications when your team has no availability",
"organization_no_slots_notification_switch_description": "Admins will get email notifications when a user tries to book a team member and is faced with 'No availability'. We trigger this email after two occurrences and remind you every 7 days per user. ",
"email_team_invite|subject|added_to_org": "{{user}} added you to the organization {{team}} on {{appName}}",
"email_team_invite|subject|invited_to_org": "{{user}} invited you to join the organization {{team}} on {{appName}}",
"email_team_invite|subject|added_to_subteam": "{{user}} added you to the team {{team}} of organization {{parentTeamName}} on {{appName}}",
"email_team_invite|subject|invited_to_subteam": "{{user}} invited you to join the team {{team}} of organization {{parentTeamName}} on {{appName}}",
"email_team_invite|subject|invited_to_regular_team": "{{user}} invited you to join the team {{team}} on {{appName}}",
"email_team_invite|heading|added_to_org": "You’ve been added to a {{appName}} organization",
"email_team_invite|heading|invited_to_org": "You’ve been invited to a {{appName}} organization",
"email_team_invite|heading|added_to_subteam": "You’ve been added to a team of {{parentTeamName}} organization",
"email_team_invite|heading|invited_to_subteam": "You’ve been invited to a team of {{parentTeamName}} organization",
"email_team_invite|heading|invited_to_regular_team": "You’ve been invited to join a {{appName}} team",
"email_team_invite|content|added_to_org": "{{invitedBy}} has added you to the <strong>{{teamName}}</strong> organization.",
"email_team_invite|content|invited_to_org": "{{invitedBy}} has invited you to join the {{teamName}} organization.",
"email_team_invite|content|added_to_subteam": "{{invitedBy}} has added you to the team <strong>{{teamName}}</strong> in their organization {{parentTeamName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
@@ -2741,6 +2741,12 @@
"month_to_date": "month to date",
"year_to_date": "year to date",
"custom_range": "custom range",
"refund_policy": "Refund Policy",
"always": "Always",
"never": "Never",
"payment_option": "Payment Option",
"if_cancelled": "If cancelled",
"before": "before",
"show_all_columns": "Show all columns",
"toggle_columns": "Toggle columns",
"no_columns_found": "No columns found",
@@ -2840,12 +2846,12 @@
"routing_form_insights_booking_status": "Booking Status",
"routing_form_insights_booking_at": "Booking At",
"routing_form_insights_submitted_at": "Submitted At",
"total_bookings_per_period":"Total Bookings per period",
"routed_to_per_period":"Routed to per period",
"total_bookings_per_period": "Total Bookings per period",
"routed_to_per_period": "Routed to per period",
"per_day": "Per day",
"per_week": "Per week",
"per_month": "Per month",
"routing_form_insights_assignment_reason":"Assignment Reason",
"routing_form_insights_assignment_reason": "Assignment Reason",
"access_denied": "Access Denied",
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
@@ -2868,7 +2874,7 @@
"rr_distribution_method_availability_description": "Allows bookers to book meetings whenever a host is available. Use this to maximize the number of potential meetings booked and when the even distribution of meetings across hosts is less important.",
"rr_distribution_method_balanced_title": "Load balancing",
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
"require_confirmation_for_free_email": "Only require confirmation for free email providers (Ex. @gmail.com, @outlook.com)",
"require_confirmation_for_free_email": "Only require confirmation for free email providers (Ex. @gmail.com, @outlook.com)",
"exclude_emails_that_contain": "Exclude emails that contain ...",
"require_emails_that_contain": "Require emails that contain ...",
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
@@ -2890,8 +2896,8 @@
"lock_attribute_for_assignment_description": "Locking would only allow assignments from Directory Sync",
"attribute_edited_successfully": "Attribute edited successfully",
"new_group_option": "New group option",
"attribute_weight_enabled":"Weights enabled",
"attribute_weight_enabled_description":"By enabling weights, it would be possible to assign higher priority to certain attributes per user. The higher the weight, the higher the priority.",
"attribute_weight_enabled": "Weights enabled",
"attribute_weight_enabled_description": "By enabling weights, it would be possible to assign higher priority to certain attributes per user. The higher the weight, the higher the priority.",
"routed": "Routed",
"reassigned": "Reassigned",
"rerouted": "Rerouted",
@@ -2903,5 +2909,13 @@
"calibration": "Calibration",
"shortfall": "Shortfall",
"routing_form": "Routing Form",
"go_back_and_save": "Go back and save",
"leave_without_saving": "Leave without saving",
"leave_without_saving_description": "Are you sure you want to leave without saving changes to your routing form?",
"something_unexpected_occurred": "Something unexpected occurred",
"500_error_message": "It's not you, it's us.",
"dry_run_mode_active": "You are doing a test booking.",
"icon_showcase": "Icon showcase",
"icons_showcase": "Icons showcase",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "La hora de finalización del evento",
"cancel_url_variable": "URL de cancelación",
"cancel_url_info": "La URL para cancelar la reserva",
"cancel_reason_variable": "Motivo de cancelación",
"cancel_reason_info": "El motivo por el cual se cancela la reserva",
"reschedule_url_variable": "URL de reprogramación",
"reschedule_url_info": "La URL para reprogramar la reserva",
"reschedule_reason_variable": "Motivo de reprogramación",
"reschedule_reason_info": "El motivo por el cual se reprograma la reserva",
"invalid_event_name_variables": "{{item}} es una variable inválida en el nombre de tu evento",
"select_all": "Seleccionar todo",
"default_conferencing_bulk_title": "Actualizar en masa los tipos de eventos existentes",
@@ -2737,6 +2741,12 @@
"month_to_date": "mes hasta la fecha",
"year_to_date": "año hasta la fecha",
"custom_range": "rango personalizado",
"refund_policy": "Política de reembolso",
"always": "Siempre",
"never": "Nunca",
"payment_option": "Opción de pago",
"if_cancelled": "Si se cancela",
"before": "antes",
"show_all_columns": "Mostrar todas las columnas",
"toggle_columns": "Alternar columnas",
"no_columns_found": "No se encontraron columnas",
@@ -2899,5 +2909,13 @@
"calibration": "Calibración",
"shortfall": "Déficit",
"routing_form": "Formulario de enrutamiento",
"go_back_and_save": "Volver y guardar",
"leave_without_saving": "Salir sin guardar",
"leave_without_saving_description": "¿Estás seguro de que quieres salir sin guardar los cambios en tu formulario de enrutamiento?",
"something_unexpected_occurred": "Ocurrió algo inesperado",
"500_error_message": "No eres tú, somos nosotros.",
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
"icon_showcase": "Muestra de íconos",
"icons_showcase": "Muestra de íconos",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "La hora de finalización del evento",
"cancel_url_variable": "URL de cancelación",
"cancel_url_info": "La URL para cancelar la reserva",
"cancel_reason_variable": "Motivo de cancelación",
"cancel_reason_info": "El motivo de la cancelación de la reserva",
"reschedule_url_variable": "URL de reprogramación",
"reschedule_url_info": "La URL para reprogramar la reserva",
"reschedule_reason_variable": "Motivo de reprogramación",
"reschedule_reason_info": "El motivo de la reprogramación de la reserva",
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
"select_all": "Seleccionar todo",
"default_conferencing_bulk_title": "Actualización masiva de los tipos de eventos existentes",
@@ -2737,6 +2741,12 @@
"month_to_date": "mes hasta la fecha",
"year_to_date": "año hasta la fecha",
"custom_range": "rango personalizado",
"refund_policy": "Política de reembolso",
"always": "Siempre",
"never": "Nunca",
"payment_option": "Opción de pago",
"if_cancelled": "Si se cancela",
"before": "antes",
"show_all_columns": "Mostrar todas las columnas",
"toggle_columns": "Alternar columnas",
"no_columns_found": "No se encontraron columnas",
@@ -2899,5 +2909,13 @@
"calibration": "Calibración",
"shortfall": "Déficit",
"routing_form": "Formulario de enrutamiento",
"go_back_and_save": "Volver y guardar",
"leave_without_saving": "Salir sin guardar",
"leave_without_saving_description": "¿Estás seguro de que quieres salir sin guardar los cambios en tu formulario de enrutamiento?",
"something_unexpected_occurred": "Algo inesperado ha ocurrido",
"500_error_message": "No eres tú, somos nosotros.",
"dry_run_mode_active": "Estás haciendo una reserva de prueba.",
"icon_showcase": "Muestra de iconos",
"icons_showcase": "Muestra de iconos",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Ürituse lõpuaeg",
"cancel_url_variable": "Tühista URL",
"cancel_url_info": "URL broneeringu tühistamiseks",
"cancel_reason_variable": "Tühistamise põhjus",
"cancel_reason_info": "Broneeringu tühistamise põhjus",
"reschedule_url_variable": "Ajasta URL ümber",
"reschedule_url_info": "URL broneeringu ümberajatamiseks",
"reschedule_reason_variable": "Ümbertõstmise põhjus",
"reschedule_reason_info": "Broneeringu ümbertõstmise põhjus",
"invalid_event_name_variables": "Teie sündmuse nimes on kehtetu muutuja",
"select_all": "Vali kõik",
"default_conferencing_bulk_title": "Olemasolevate sündmuste tüüpide hulgivärskendus",
@@ -2737,6 +2741,12 @@
"month_to_date": "kuu algusest tänaseni",
"year_to_date": "aasta algusest tänaseni",
"custom_range": "kohandatud vahemik",
"refund_policy": "Tagastuspoliitika",
"always": "Alati",
"never": "Mitte kunagi",
"payment_option": "Makseviis",
"if_cancelled": "Kui tühistatud",
"before": "enne",
"show_all_columns": "Näita kõiki veerge",
"toggle_columns": "Lülita veerud sisse/välja",
"no_columns_found": "Veerge ei leitud",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibreerimine",
"shortfall": "Puudujääk",
"routing_form": "Marsruutimise vorm",
"go_back_and_save": "Mine tagasi ja salvesta",
"leave_without_saving": "Lahku salvestamata",
"leave_without_saving_description": "Kas oled kindel, et soovid lahkuda marsruutimisvormi muudatusi salvestamata?",
"something_unexpected_occurred": "Midagi ootamatut juhtus",
"500_error_message": "Viga ei ole sinus, vaid meis.",
"dry_run_mode_active": "Teed proovibroneeringut.",
"icon_showcase": "Ikoonide ülevaade",
"icons_showcase": "Ikoonide ülevaade",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Gertaeraren amaiera ordua",
"cancel_url_variable": "Bertan behera uzteko URLa",
"cancel_url_info": "Erreserba bertan behera uzteko URLa",
"cancel_reason_variable": "Bertan behera uzteko arrazoia",
"cancel_reason_info": "Erreserba bertan behera uzteko arrazoia",
"reschedule_url_variable": "Berriz programatzeko URLa",
"reschedule_url_info": "Erreserba berriz programatzeko URLa",
"reschedule_reason_variable": "Birprogramatzeko arrazoia",
"reschedule_reason_info": "Erreserba birprogramatzeko arrazoia",
"invalid_event_name_variables": "{{item}} aldagai baliogabea da zure gertaeraren izenean",
"select_all": "Hautatu guztiak",
"default_conferencing_bulk_title": "Eguneratu masiboki existitzen diren gertaera motak",
@@ -2737,6 +2741,12 @@
"month_to_date": "hilabetea gaur arte",
"year_to_date": "urtea gaur arte",
"custom_range": "pertsonalizatutako tartea",
"refund_policy": "Itzulketa politika",
"always": "Beti",
"never": "Inoiz ez",
"payment_option": "Ordainketa aukera",
"if_cancelled": "Bertan behera utziz gero",
"before": "aurretik",
"show_all_columns": "Erakutsi zutabe guztiak",
"toggle_columns": "Aldatu zutabeak",
"no_columns_found": "Ez da zutaberik aurkitu",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrazioa",
"shortfall": "Gabezia",
"routing_form": "Bideratze-formularioa",
"go_back_and_save": "Itzuli eta gorde",
"leave_without_saving": "Irten gorde gabe",
"leave_without_saving_description": "Ziur zaude bideratzeko inprimakian egindako aldaketak gorde gabe irten nahi duzula?",
"something_unexpected_occurred": "Ustekabeko zerbait gertatu da",
"500_error_message": "Ez zara zu, gu gara",
"dry_run_mode_active": "Proba-erreserba bat egiten ari zara.",
"icon_showcase": "Ikonoen erakusleihoa",
"icons_showcase": "Ikonoen erakusleihoa",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2031,8 +2031,12 @@
"event_end_time_info": "Tapahtuman päättymisaika",
"cancel_url_variable": "Peruutus-URL",
"cancel_url_info": "URL-osoite varauksen peruuttamiseen",
"cancel_reason_variable": "Peruutuksen syy",
"cancel_reason_info": "Varauksen peruuttamisen syy",
"reschedule_url_variable": "Uudelleenajoitus-URL",
"reschedule_url_info": "URL-osoite varauksen uudelleenajoittamiseen",
"reschedule_reason_variable": "Uudelleenvarauksen syy",
"reschedule_reason_info": "Varauksen uudelleenajoituksen syy",
"invalid_event_name_variables": "{{item}} on virheellinen muuttuja tapahtuman nimessä",
"select_all": "Valitse kaikki",
"default_conferencing_bulk_title": "Päivitä olemassa olevat tapahtumatyypit massana",
@@ -2737,6 +2741,12 @@
"month_to_date": "kuukauden alusta tähän päivään",
"year_to_date": "vuoden alusta tähän päivään",
"custom_range": "mukautettu aikaväli",
"refund_policy": "Palautuskäytäntö",
"always": "Aina",
"never": "Ei koskaan",
"payment_option": "Maksutapa",
"if_cancelled": "Jos peruutettu",
"before": "ennen",
"show_all_columns": "Näytä kaikki sarakkeet",
"toggle_columns": "Näytä/piilota sarakkeet",
"no_columns_found": "Sarakkeita ei löytynyt",
@@ -2899,5 +2909,13 @@
"calibration": "Kalibrointi",
"shortfall": "Vajaus",
"routing_form": "Reitityskaavake",
"go_back_and_save": "Palaa takaisin ja tallenna",
"leave_without_saving": "Poistu tallentamatta",
"leave_without_saving_description": "Haluatko varmasti poistua tallentamatta muutoksia reitityslomaakkeeseesi?",
"something_unexpected_occurred": "Jotain odottamatonta tapahtui",
"500_error_message": "Vika ei ole sinussa, vaan meissä.",
"dry_run_mode_active": "Teet testikäyntivarausta.",
"icon_showcase": "Kuvakegalleria",
"icons_showcase": "Kuvakegalleria",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

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