Compare commits

..
44 Commits
Author SHA1 Message Date
GitHub Actions c581456586 chore: release v4.8.19
Post release / release (push) Canceled after 0s
2025-01-15 15:30:52 +00:00
36dd3e9e6c fix: disable custom templates for free plan (#18635)
* add upgrade badge

* adjust update handler

* code clean up

* clean up template code

* only handle email templates

* fixes

* code clean up

* fix workflow template zod type

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-15 15:25:14 +00:00
Alex van Andel f137aee436 Remove export keyword @ route - only default allowed (#18682) 2025-01-15 14:33:02 +00:00
Eunjae Lee 4f494d5a32 refactor: use DataTable for /bookings (#18589)
* refactor: use DataTable for /bookings

* remove unnecessary animation (for better scroll performance)

* add consistent search box
2025-01-15 14:29:50 +00:00
Benny Joo cce6e83578 chore: migrate /org team pages and team type pages and user type page (#18668) 2025-01-15 09:21:21 +00:00
Lauris Skraucis c21ba636d2 fix: Booker atom name booking field (#18653)
* feat: split name booking field in v2

* fix: booker atom handle split name

* fix: after and before start dates bookings filter

* chore: bump platform libraries
2025-01-15 14:08:06 +05:30
Benny Joo 5ccf5e6be8 chore: migrate /team booking pages (#18656)
* wip

* remove pages/team/[slug]

* fix tests

* fix tests 2
2025-01-14 23:48:41 +00:00
Joe Au-YeungandAlex van Andel 66b3e735d8 feat: Routing form submitted but no booking - Salesforce actions (#18616)
* Add booking incomplete actions table

* Add incomplete booking page tab

* Add `getincompleteBookingSettings` trpc endpoints

* Add incomplete booking page

* Abstract enabled apps array

* Handle no enabled credentials and no actions

* Add enabled field to incomplete booking action db record

* Add new write record entries

* UI add separation between switch and inputs

* Fix typo

* clean up

* Add saveIncompleteBookingSettings endpoint

* Save incomplete booking settings

* Fix language around when to write to field

* Add `credentialId` to action record

* Choose which credential to assign to action

* Save credential to action

* Revert "Save credential to action"

This reverts commit ba6a1c808baed54f6d3d4c6155cf192c813d0696.

* Revert "Choose which credential to assign to action"

This reverts commit 968f6e5295d098c64abe95268afd1fa139a175ba.

* Revert "Add `credentialId` to action record"

This reverts commit 579f9ff4167b65aa987659e4e8f8c26f9e773317.

* Add credentialId to action record - rewrite migration file

* Revert "Add credentialId to action record - rewrite migration file"

This reverts commit 2843a92c61820e7f7a1614a557d3f7ea96342107.

* Revert "Add booking incomplete actions table"

This reverts commit 7ec75bef4a0f0a9d07be0142da64c49d739439ea.

* Revert "Add enabled field to incomplete booking action db record"

This reverts commit d279a1da05819eafa8fc5d664e3be733e0687e8d.

* Write migration in single commit

* Rename table

* Rename table - remove underscores

* Remove credential relationship

* Type fix - changing table name

* Fix table name

* Change writeToRecordObject to object

* Salesforce add incomplete booking, write to record

* Add incomplete booking actions to `triggerFormSubmittedNoEventWebhooks`

* Remove console.log

* Type fixes

* Type fixes

* Iterate if incompleteBookingActions

* Choose which credential to assign to action

* Save credential to action

* Fix getServerSideProp changes

* Type fix

* Type fix

* Type fix

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-14 23:17:16 +00:00
Praash 7a8b9ededb fix: html render from calendar invite (#18577) 2025-01-14 20:40:31 +00:00
Benny Joo 501c49f369 perf: no need for two query fetches for d link (private booking page) (#18659)
* return eventData

* use eventData and do not do extra query fetch
2025-01-14 17:15:56 +00:00
Amit Sharma 536604455b fix: BUG: adding filters to settings/teams/[team-id]/members page throws error (#18631) 2025-01-14 16:30:54 +00:00
MorganandPeer Richelsen cfb76a9689 chore: platform libraries readme publish reasons (#18646)
* chore: platform libraries readme publish reasons

* fixup! chore: platform libraries readme publish reasons

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-14 16:28:34 +00:00
Udit TakkarandPeer Richelsen 30180edc0b fix: phone number in confirmation email (#18642)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-01-14 16:27:24 +00:00
MorganandCarina Wollendorfer 2aa3d1d4e0 feat: get org ooo entries and filters/sort (#18645)
* feat: get org ooo entries and filters/sort

* remove console.log

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2025-01-14 15:33:22 +00:00
Eunjae Lee 21694c039a perf: improve routing insights view (2) (#18566) 2025-01-14 12:28:39 -03:00
Benny Joo a8c29b5459 chore: log error to Sentry in app router error page (#18637) 2025-01-14 15:23:07 +00:00
Anik Dhabal Babu 6cd7454f43 fix: /booking page datefilter (#18654)
* fix: /booking page datefilter

* Update get.handler.ts

* Update get.handler.ts

* shit
2025-01-14 15:14:28 +00:00
Calcom BotandReplexica 99b2419dd7 feat: update translations via @replexica (#18598)
* chore: sync with main

* feat: update translations via @replexica

---------

Co-authored-by: Replexica <support@replexica.com>
2025-01-14 13:47:22 +00:00
Benny Joo f6d0a2ca09 fix: potential fix for flaky layout shift (#18651)
* potential fix: layout shift

* fix lint error
2025-01-14 13:36:20 +00:00
Alex van Andel f5ab9bd40b Allow lint to error but continue (unblock pipeline) 2025-01-14 13:12:24 +00:00
Benny JooandAlex van Andel 07085b9ffb chore: clean up config.matcher in middleware (#18638)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-14 10:24:36 +00:00
Benny Joo 14151b827f fix: make ESLint work and fix lint errors that were undetected before (#18639)
* fix eslint config

* fix

* add it to dev dep

* fix

* sync eslint version

* force ts-node to compile our ESLint plugin's TS files into CommonJS (which ESLint requires)

* fix some lint errors

* fix lint errors

* remove duplicate classname

* make @typescript-eslint/ban-types a warn for packages/trpc files

* fix lint errors in trpc

* fix lint errors in trpc - 2

* fix

* fix

* fix lint warnings
2025-01-14 08:58:38 +00:00
Kartik Saini 28b757f19c feat: do not show automation webhooks in webhook list (#18607) 2025-01-14 10:55:22 +05:30
Alex van Andel 68455db860 fix: main lint errors (#18634)
* fix: error-page.tsx related lint errors

* fix: lint no continue-on-error

* Adding more lint fixes

* Bring back annotate code linting results

* Bring back linting continue-on-error

* Slimline lint
2025-01-13 22:01:20 +00:00
Udit Takkar 96ff88d266 fix: disable sending sms when email is present (#18632)
* fix: disable sending sms when email is present

* fix: update test
2025-01-13 20:38:44 +00:00
GitHub Actions f88a91dad0 chore: release v4.8.18
Post release / release (push) Canceled after 0s
2025-01-13 18:12:32 +00:00
Keith WilliamsandAlex van Andel 35059d7727 revert: "feat: bulk shorten links with dub.links.createMany (#18539)" (#18587)
This reverts commit 4902c6a0f8.

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-01-13 18:07:27 +00:00
Carina WollendorferandCarinaWolli 68f76e2acc disable emails to all guests (#18628)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-13 15:58:43 +00:00
Benny Joo 80aac06ff3 chore: restore error pages for pages router (#18625) 2025-01-13 12:42:00 +00:00
sean-brydon 0f47528ba2 fix: parsing teamId (#18623) 2025-01-13 12:16:59 +00:00
Benny Joo 113dc5fca6 chore: try using custom 404 in pages/_error (#18622) 2025-01-13 11:35:10 +00:00
Benny Joo 0befdde5d7 chore: gracefully handle 404s from pages router's dynamic pages + tests (#18618)
* restore pages/_error

* set custom header in pages/_error

* handle it in middleware

* add test

* remove logs

* better test description
2025-01-13 15:46:56 +05:30
Benny Joo b3ed6e39e3 chore: redirect to /500 if pathname does not exist + better error handling (#18615)
* fix lint error

* fix booking page and better error handling
2025-01-13 00:04:36 +00:00
sean-brydon e07b1eda32 Async False (#18611) 2025-01-12 20:31:43 +00:00
Benny Joo 8262894bde fix: app/not-found cannot be a static page (#18610) 2025-01-12 20:21:03 +01:00
Benny Joo a3d7bcd22e feat: render custom error page for unexpected sever error + remove pages/_error (#18606)
* remove page/_error

* refactor app/error
2025-01-12 14:12:02 +01:00
Omar LópezandBenny Joo 62b753afba chore: app router 404 page (#18597)
* wip

* wip

* fix not found page

* render middleware for /settings pages

* fix

* remove global-error page

* add metadata to not-found page

* make not-found page static

* remove 404

* adding not-found to middleware is not necessary

* add every routes to config.matcher

* fix test

* fix style

* use i18n string

* fix tests

* fix

* fix

* revert unneeded changes

* fix

* fix

* fix

* fix style

* fix

* remove 404

* remove log

* fix

* fix

* fix

* fix

* better naming

* parallel testing

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-01-11 02:42:08 +00:00
bb40b2f6bf chore: app router - all sub-pages in /apps (#16976)
* chore: apps/[slug] remove pages router

* remove apps/[slug] pages from /future

* chore: apps/installed remove pages router

* chore: apps/installation remove pages router

* remove Head element

* fix metadata

* fix test

* fix another test

* chore: apps/categories remove pages router

* revert unneeded changes

* update middleware

* Remove <Head>

* remove unused import and code

* remove unused import and code again

* fix

* fix category page

* add split icon

* add /routing paths to middleware matcher

* wip

* remove HeadSeo from App.tsx

* clean up head-seo test

* add generateAppMetadata

* use generateAppMetadata in apps/[slug] page

* delete file

* remove log

* fix

* fix

* fix apps/installed pages

* fix cateogires pages

* fix

* fix imports

* wip

* fix

* fix

* fix metadata

* fix

* redirect /apps/routing-forms to /routing

* replace all usages of /apps/routing-forms to /routing

* better naming

* /routing -> /routing/forms

* fix

* fix

* fix

* fix

* remove backPath as it is irrelevant when withoutMain is true

* fix type checks

* fix type check in apps/[slug]

* refactors

* fix

* fix test

* fix

* fix

* fix

* Replace multiple leading slashes with a single slash

* migrate routing-forms too

* add re routing

* fix

* add redirection

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-10 20:16:11 -05:00
NizzyandOmar López f628dc83a8 detailed customer card (#18511)
Co-authored-by: Omar López <zomars@me.com>
2025-01-10 23:51:27 +00:00
sean-brydonandOmar López 2fd2578504 feat: Round Robin weights future members toggle (#17782)
Co-authored-by: Omar López <zomars@me.com>
2025-01-10 14:24:19 -07:00
Syed Ali ShahbazandCarinaWolli a29407349d docs: fix error introduced in #18578 (#18593)
* Fix error

* remove indent

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-01-10 19:48:12 +00:00
Joe Au-YeungandAnik Dhabal Babu e7df1ef493 fix: Create dub links if valid url (#18588)
* Create dub links if valid

* Map through filtered links

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-01-10 15:58:16 +00:00
Kartik Saini b652ae7714 fix: wrong Meeting Ended payload in zapier (#18202)
* fix: wrong Meeting Ended payload in zapier

* fix
2025-01-10 15:55:35 +01:00
Peer Richelsen cec1c9ee46 feat: granola.ai app (#18585)
* new granola app

* granola app
2025-01-10 14:47:18 +00:00
253 changed files with 4194 additions and 1845 deletions
-8
View File
@@ -355,14 +355,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
# provide a value between 0 and 100 to ensure the percentage of traffic
# redirected from the legacy to the future pages
AB_TEST_BUCKET_PROBABILITY=50
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
APP_ROUTER_APPS_SLUG_ENABLED=0
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
# whether we redirect to the future/apps/categories from /apps/categories or not
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
+2 -12
View File
@@ -12,16 +12,6 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- name: Run Linting with Reports
run: yarn lint:report
- name: Run Lint
run: yarn lint
continue-on-error: true
- name: Merge lint reports
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
- name: Upload ESLint report
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: lint-results
path: lint-results
+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"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@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.81",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.82",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -3,6 +3,8 @@ import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
import { SkipTakePagination } from "@calcom/platform-types";
export enum OutOfOfficeReason {
UNSPECIFIED = "unspecified",
VACATION = "vacation",
@@ -80,3 +82,47 @@ export class CreateOutOfOfficeEntryDto {
}
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
export enum SortOrder {
asc = "asc",
desc = "desc",
}
type SortOrderType = keyof typeof SortOrder;
export class GetOutOfOfficeEntryFiltersDTO extends SkipTakePagination {
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortStart must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their start time in ascending or descending order.",
example: "?sortStart=asc OR ?sortStart=desc",
enum: SortOrder,
})
sortStart?: SortOrderType;
@IsOptional()
@IsEnum(SortOrder, {
message: 'SortEnd must be either "asc" or "desc".',
})
@ApiProperty({
required: false,
description: "Sort results by their end time in ascending or descending order.",
example: "?sortEnd=asc OR ?sortEnd=desc",
enum: SortOrder,
})
sortEnd?: SortOrderType;
}
export class GetOrgUsersOutOfOfficeEntryFiltersDTO extends GetOutOfOfficeEntryFiltersDTO {
@IsString()
@IsOptional()
@ApiProperty({
type: String,
required: false,
description: "Filter ooo entries by the user email address. user must be within your organization.",
example: "example@domain.com",
})
email?: string;
}
@@ -46,12 +46,19 @@ export class UserOOORepository {
});
}
async getUserOOOPaginated(userId: number, skip: number, take: number) {
async getUserOOOPaginated(
userId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
) {
return this.dbRead.prisma.outOfOfficeEntry.findMany({
where: { userId },
skip,
take,
include: { reason: true },
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
});
}
@@ -2,6 +2,8 @@ import {
CreateOutOfOfficeEntryDto,
UpdateOutOfOfficeEntryDto,
OutOfOfficeReason,
GetOutOfOfficeEntryFiltersDTO,
SortOrder,
} from "@/modules/ooo/inputs/ooo.input";
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -128,8 +130,13 @@ export class UserOOOService {
return this.formatOooReason(ooo);
}
async getUserOOOPaginated(userId: number, skip: number, take: number) {
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take);
async getUserOOOPaginated(
userId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" }
) {
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take, sort);
return ooos.map((ooo) => this.formatOooReason(ooo));
}
}
@@ -8,13 +8,19 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto } from "@/modules/ooo/inputs/ooo.input";
import {
CreateOutOfOfficeEntryDto,
UpdateOutOfOfficeEntryDto,
GetOutOfOfficeEntryFiltersDTO,
GetOrgUsersOutOfOfficeEntryFiltersDTO,
} from "@/modules/ooo/inputs/ooo.input";
import {
UserOooOutputDto,
UserOooOutputResponseDto,
UserOoosOutputResponseDto,
} from "@/modules/ooo/outputs/ooo.output";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
import {
Controller,
UseGuards,
@@ -33,10 +39,9 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/users/:userId/ooo",
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseInterceptors(ClassSerializerInterceptor)
@@ -44,18 +49,22 @@ import { SkipTakePagination } from "@calcom/platform-types";
@UseGuards(IsOrgGuard)
@DocsTags("Orgs / Users / OOO")
export class OrganizationsUsersOOOController {
constructor(private readonly userOOOService: UserOOOService) {}
constructor(
private readonly userOOOService: UserOOOService,
private readonly orgUsersOOOService: OrgUsersOOOService
) {}
@Get()
@Get("/users/:userId/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg)
@ApiOperation({ summary: "Get all ooo entries of a user" })
async getOrganizationUserOOO(
@Param("userId", ParseIntPipe) userId: number,
@Query() query: SkipTakePagination
@Query() query: GetOutOfOfficeEntryFiltersDTO
): Promise<UserOoosOutputResponseDto> {
const ooos = await this.userOOOService.getUserOOOPaginated(userId, query.skip ?? 0, query.take ?? 250);
const { skip, take, ...rest } = query ?? { skip: 0, take: 250 };
const ooos = await this.userOOOService.getUserOOOPaginated(userId, skip ?? 0, take ?? 250, rest);
return {
status: SUCCESS_STATUS,
@@ -63,7 +72,7 @@ export class OrganizationsUsersOOOController {
};
}
@Post()
@Post("/users/:userId/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg)
@@ -79,7 +88,7 @@ export class OrganizationsUsersOOOController {
};
}
@Patch("/:oooId")
@Patch("/users/:userId/ooo/:oooId")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg, IsUserOOO)
@@ -97,7 +106,7 @@ export class OrganizationsUsersOOOController {
};
}
@Delete("/:oooId")
@Delete("/users/:userId/ooo/:oooId")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsUserInOrg, IsUserOOO)
@@ -111,4 +120,23 @@ export class OrganizationsUsersOOOController {
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
};
}
@Get("/ooo")
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@ApiOperation({ summary: "Get all OOO entries of org users" })
async getOrganizationUsersOOO(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() query: GetOrgUsersOutOfOfficeEntryFiltersDTO
): Promise<UserOoosOutputResponseDto> {
const { skip, take, email, ...rest } = query ?? { skip: 0, take: 250 };
const ooos = await this.orgUsersOOOService.getOrgUsersOOOPaginated(orgId, skip ?? 0, take ?? 250, rest, {
email,
});
return {
status: SUCCESS_STATUS,
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
};
}
}
@@ -366,6 +366,31 @@ describe("Organizations User OOO Endpoints", () => {
});
});
it("should get 2 ooo entries", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/ooo?sortEnd=desc&email=${teammate1Email}`)
.expect(200)
.then((response) => {
const responseBody = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data as UserOooOutputDto[];
expect(data.length).toEqual(2);
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
expect(oooUno).toBeDefined();
if (oooUno) {
expect(oooUno.reason).toEqual("vacation");
expect(oooUno.toUserId).toEqual(teammate2.id);
expect(oooUno.userId).toEqual(teammate1.id);
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
}
// test sort
expect(data[1].id).toEqual(oooCreatedViaApiId);
});
});
it("should get 2 ooo entries", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
@@ -0,0 +1,33 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrgUsersOOORepository {
constructor(private readonly dbRead: PrismaReadService) {}
async getOrgUsersOOOPaginated(
orgId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
filters?: { email?: string }
) {
console.log({ sort, filters });
return this.dbRead.prisma.outOfOfficeEntry.findMany({
where: {
user: {
...(filters?.email && { email: filters.email }),
profiles: {
some: {
organizationId: orgId,
},
},
},
},
skip,
take,
include: { reason: true },
...(sort?.sortStart && { orderBy: { start: sort.sortStart } }),
...(sort?.sortEnd && { orderBy: { end: sort.sortEnd } }),
});
}
}
@@ -0,0 +1,26 @@
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrgUsersOOOService {
constructor(
private readonly oooRepository: UserOOORepository,
private readonly oooUserService: UserOOOService,
private readonly usersRepository: UsersRepository,
private readonly orgUsersOOORepository: OrgUsersOOORepository
) {}
async getOrgUsersOOOPaginated(
orgId: number,
skip: number,
take: number,
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
filters?: { email?: string }
) {
const ooos = await this.orgUsersOOORepository.getOrgUsersOOOPaginated(orgId, skip, take, sort, filters);
return ooos.map((ooo) => this.oooUserService.formatOooReason(ooo));
}
}
@@ -15,6 +15,8 @@ import { OrganizationsTeamsMembershipsController } from "@/modules/organizations
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
import { OrgUsersOOORepository } from "@/modules/organizations/controllers/users/ooo/repositories/organizations-users-ooo.repository";
import { OrgUsersOOOService } from "@/modules/organizations/controllers/users/ooo/services/organization-users-ooo.service";
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
@@ -91,6 +93,8 @@ import { Module } from "@nestjs/common";
OutputTeamEventTypesResponsePipe,
UserOOOService,
UserOOORepository,
OrgUsersOOOService,
OrgUsersOOORepository,
],
exports: [
OrganizationsService,
@@ -1,12 +1,25 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional, Validate } from "class-validator";
import { SkipTakePagination } from "@calcom/platform-types";
import { IsNumber, IsOptional, Max, Min, Validate } from "class-validator";
import { IsEmailStringOrArray } from "../validators/isEmailStringOrArray";
export class GetUsersInput extends SkipTakePagination {
export class GetUsersInput {
@ApiProperty({ required: false, description: "The number of items to return", example: 10 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@Max(1000)
@IsOptional()
take?: number;
@ApiProperty({ required: false, description: "The number of items to skip", example: 0 })
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@IsOptional()
skip?: number;
@IsOptional()
@Validate(IsEmailStringOrArray)
@Transform(({ value }: { value: string | string[] }) => {
+115 -2
View File
@@ -2516,6 +2516,8 @@
"description": "The number of items to return",
"example": 10,
"schema": {
"minimum": 1,
"maximum": 1000,
"type": "number"
}
},
@@ -2526,6 +2528,7 @@
"description": "The number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"type": "number"
}
},
@@ -2716,6 +2719,34 @@
"schema": {
"type": "number"
}
},
{
"name": "sortStart",
"required": false,
"in": "query",
"description": "Sort results by their start time in ascending or descending order.",
"example": "?sortStart=asc OR ?sortStart=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortEnd",
"required": false,
"in": "query",
"description": "Sort results by their end time in ascending or descending order.",
"example": "?sortEnd=asc OR ?sortEnd=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
}
],
"responses": {
@@ -2824,6 +2855,88 @@
]
}
},
"/v2/organizations/{orgId}/ooo": {
"get": {
"operationId": "OrganizationsUsersOOOController_getOrganizationUsersOOO",
"summary": "Get all OOO entries of org users",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "The number of items to return",
"example": 10,
"schema": {
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "The number of items to skip",
"example": 0,
"schema": {
"type": "number"
}
},
{
"name": "sortStart",
"required": false,
"in": "query",
"description": "Sort results by their start time in ascending or descending order.",
"example": "?sortStart=asc OR ?sortStart=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "sortEnd",
"required": false,
"in": "query",
"description": "Sort results by their end time in ascending or descending order.",
"example": "?sortEnd=asc OR ?sortEnd=desc",
"schema": {
"enum": [
"asc",
"desc"
],
"type": "string"
}
},
{
"name": "email",
"required": false,
"in": "query",
"description": "Filter ooo entries by the user email address. user must be within your organization.",
"example": "example@domain.com",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Orgs / Users / OOO"
]
}
},
"/v2/organizations/{orgId}/webhooks": {
"get": {
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
@@ -7888,7 +8001,7 @@
"type": {
"type": "string",
"example": "name",
"description": "only allowed value for type is `name`"
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name."
},
"label": {
"type": "string"
@@ -8682,7 +8795,7 @@
"boolean"
],
"example": "name",
"description": "only allowed value for type is `name`",
"description": "only allowed value for type is `name`. Used for having 1 booking field for both first name and last name.",
"default": "name"
},
"label": {
-5
View File
@@ -5,11 +5,6 @@ import { NextResponse, URLPattern } from "next/server";
import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME } from "@calcom/lib/constants";
const ROUTES: [URLPattern, boolean][] = [
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const,
["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const,
["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const,
+45 -4
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 { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
import { constructGenericImage, constructAppImage, 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";
@@ -61,17 +62,18 @@ const _generateMetadataWithoutImage = async (
const t = await getTranslationWithCache(locale);
const title = getTitle(t);
const description = getDescription(t);
const titleSuffix = `| ${APP_NAME}`;
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
const truncatedDescription = truncateOnWord(getDescription(t), 158);
return {
title: title.length === 0 ? APP_NAME : displayedTitle,
description,
description: truncatedDescription,
alternates: { canonical },
openGraph: {
description: truncateOnWord(description, 158),
description: truncatedDescription,
url: canonical,
type: "website",
siteName: APP_NAME,
@@ -103,3 +105,42 @@ export const _generateMetadata = async (
},
};
};
export const generateAppMetadata = async (
app: AppImageProps,
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 + constructAppImage({ ...app, description: metadata.description });
return {
...metadata,
openGraph: {
...metadata.openGraph,
images: [image],
},
};
};
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],
},
};
};
+22 -163
View File
@@ -10,12 +10,18 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
const inputSchema = z.object({
customer: z.object({
name: z.string().optional(),
email: z.string().email(),
username: z.string().optional(),
timeZone: z.string().optional(),
emailVerified: z.boolean().optional(),
identityProvider: z.string().optional(),
twoFactorEnabled: z.boolean().optional(),
lastActiveAt: z.string().optional(),
teamName: z.string().optional(),
teamSlug: z.string().optional(),
isOrganization: z.boolean().optional(),
stripeCustomerId: z.string().optional(),
}),
cardKeys: z.array(z.string()),
});
@@ -53,168 +59,10 @@ async function handler(request: Request) {
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: customer.email || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email Verified?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: customer.emailVerified ? "Yes" : "No",
badgeColor: customer.emailVerified ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Username",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: customer.username || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "User ID",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Time Zone",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: customer.timeZone || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Two Factor Enabled?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: customer.twoFactorEnabled ? "Yes" : "No",
badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Identity Provider",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: customer.identityProvider || "Unknown",
},
},
],
componentText: {
textSize: "L",
textColor: "MUTED",
text: "User does not exist!",
},
},
],
@@ -223,18 +71,29 @@ async function handler(request: Request) {
});
}
// Fetch team details including userId and team name
const teamMemberships = await UserRepository.findTeamsByUserId({ userId: user.id });
const firstTeam = teamMemberships.teams[0] ?? null;
// Parse user metadata
const parsedMetadata = userMetadata.parse(user.metadata);
const cards = await Promise.all(
cardExamples.map(async (cardFn) => {
return cardFn(
user.name || "Unknown",
user.email,
user.id.toString(),
user.username || "Unknown",
user.timeZone,
user.emailVerified,
user.twoFactorEnabled
user.twoFactorEnabled,
user.identityProvider,
user.lastActiveAt,
firstTeam?.name || "Unknown",
firstTeam?.slug || "Unknown",
firstTeam?.isOrganization || false,
(user.metadata as { stripeCustomerId?: string })?.stripeCustomerId || "Unknown"
);
})
);
+68
View File
@@ -0,0 +1,68 @@
import type { PageProps as _PageProps } from "app/_types";
import { generateAppMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import { z } from "zod";
import { AppRepository } from "@calcom/lib/server/repository/app";
import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check";
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
import AppView from "~/apps/[slug]/slug-view";
const paramsSchema = z.object({
slug: z.string(),
});
export const generateMetadata = async ({ params }: _PageProps) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const props = await getStaticProps(p.data.slug);
if (!props) {
notFound();
}
const { name, logo, description } = props.data;
return await generateAppMetadata(
{ slug: logo, name, description },
() => name,
() => description
);
};
export const generateStaticParams = async () => {
const isPrismaAvailable = await isPrismaAvailableCheck();
if (!isPrismaAvailable) {
// Database is not available at build time. Make sure we fall back to building these pages on demand
return [];
}
const appStore = await AppRepository.findAppStore();
return appStore.map(({ slug }) => ({ slug }));
};
async function Page({ params }: _PageProps) {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const props = await getStaticProps(p.data.slug);
if (!props) {
notFound();
}
return <AppView {...props} />;
}
export default WithLayout({ getLayout: null, ServerPage: Page });
export const dynamic = "force-static";
@@ -2,16 +2,34 @@ import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
import Page, { type PageProps } from "~/apps/[slug]/setup/setup-view";
const paramsSchema = z.object({
slug: z.string(),
});
export const generateMetadata = async ({ params }: _PageProps) => {
return await _generateMetadata(
() => `${params.slug}`,
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const metadata = await _generateMetadata(
() => `${p.data.slug}`,
() => ""
);
return {
...metadata,
robots: {
follow: false,
index: false,
},
};
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
@@ -0,0 +1,40 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { redirect } from "next/navigation";
import { z } from "zod";
import { AppCategories } from "@calcom/prisma/enums";
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
import CategoryPage from "~/apps/categories/[category]/category-view";
const querySchema = z.object({
category: z.enum(Object.values(AppCategories) as [AppCategories, ...AppCategories[]]),
});
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("app_store"),
(t) => t("app_store_description")
);
};
export const generateStaticParams = async () => {
const paths = Object.keys(AppCategories);
return paths.map((category) => ({ category }));
};
async function Page({ params, searchParams }: PageProps) {
const parsed = querySchema.safeParse({ ...params, ...searchParams });
if (!parsed.success) {
redirect("/apps/categories/calendar");
}
const props = await getStaticProps(parsed.data.category);
return <CategoryPage {...props} />;
}
export default WithLayout({ getLayout: null, ServerPage: Page });
@@ -12,7 +12,6 @@ import Page from "~/apps/installation/[[...step]]/step-view";
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { appMetadata } = await getData(legacyCtx);
return await _generateMetadata(
(t) => `${t("install")} ${appMetadata?.name ?? ""}`,
@@ -0,0 +1,32 @@
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { redirect } from "next/navigation";
import { z } from "zod";
import { AppCategories } from "@calcom/prisma/enums";
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
const querySchema = z.object({
category: z.nativeEnum(AppCategories),
});
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("installed_apps"),
(t) => t("manage_your_connected_apps")
);
};
const InstalledAppsWrapper = async ({ params }: PageProps) => {
const parsedParams = querySchema.safeParse(params);
if (!parsedParams.success) {
redirect("/apps/installed/calendar");
}
return <InstalledApps category={parsedParams.data.category} />;
};
export default WithLayout({ getLayout: null, ServerPage: InstalledAppsWrapper });
@@ -0,0 +1 @@
export { default } from "app/routing-forms/[...pages]/page";
+1
View File
@@ -0,0 +1 @@
export { default } from "app/routing-forms/page";
+5 -17
View File
@@ -4,9 +4,6 @@ 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/d/[link]/[slug]/getServerSideProps";
import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
@@ -17,24 +14,15 @@ 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, eventData, isBrandingHidden } = pageProps;
const rescheduleUid = booking?.uid;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(legacyCtx.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
const event = await EventRepository.getPublicEvent({
username,
eventSlug,
isTeamEvent,
org,
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
});
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
const profileName = eventData?.profile?.name ?? "";
const title = eventData?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
isBrandingHidden
);
};
+45 -50
View File
@@ -1,64 +1,59 @@
"use client";
/**
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/
import type { NextPage } from "next";
import type { ErrorProps } from "next/error";
import { captureException } from "@sentry/nextjs";
import React from "react";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { redactError } from "@calcom/lib/redactError";
import { ErrorPage } from "@components/error/error-page";
type NextError = Error & { digest?: string };
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
export type DefaultErrorProps = {
error: NextError;
reset: () => void; // A function to reset the error boundary
};
type AugmentedError = NextError | HttpError | null;
type CustomErrorProps = {
err?: AugmentedError;
statusCode?: number;
message?: string;
} & Omit<ErrorProps, "err" | "statusCode">;
const log = logger.getSubLogger({ prefix: ["[error]"] });
const CustomError: NextPage<DefaultErrorProps> = (props) => {
const { error } = props;
let errorObject: CustomErrorProps = {
message: error.message,
err: error,
};
if (error instanceof HttpError) {
const redactedError = redactError(error);
errorObject = {
statusCode: error.statusCode,
title: redactedError.name,
message: redactedError.message,
err: {
...redactedError,
...error,
},
};
}
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
log.info("errorObject: ", errorObject);
return (
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
);
type ErrorProps = {
error: Error;
reset: () => void;
};
export default CustomError;
export default function Error({ error, reset }: ErrorProps) {
React.useEffect(() => {
log.error(error);
// Log the error to Sentry
captureException(error);
}, [error]);
const processedError = React.useMemo(() => {
const err = getErrorFromUnknown(error);
if (err instanceof HttpError) {
const redactedError = redactError(err);
return {
statusCode: err.statusCode,
title: redactedError.name,
name: redactedError.name,
message: redactedError.message,
url: err.url,
method: err.method,
cause: err.cause,
};
}
return {
statusCode: 500,
title: "Internal Server Error",
name: "Internal Server Error",
message: "An unexpected error occurred.",
};
}, [error]);
return (
<ErrorPage
statusCode={processedError.statusCode}
error={processedError}
message={processedError.message}
/>
);
}
-45
View File
@@ -1,45 +0,0 @@
import { Prisma } from "@prisma/client";
import { withAppDirSsg } from "app/WithAppDirSsg";
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 { AppRepository } from "@calcom/lib/server/repository/app";
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import type { PageProps } from "~/apps/[slug]/slug-view";
import Page from "~/apps/[slug]/slug-view";
const getData = withAppDirSsg<PageProps>(getStaticProps, "future/apps/[slug]");
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
const res = await getData(legacyContext);
return await _generateMetadata(
() => res?.data.name ?? "",
() => res?.data.description ?? ""
);
};
export const generateStaticParams = async () => {
try {
const appStore = await AppRepository.findAppStore();
return appStore.map(({ slug }) => ({ slug }));
} catch (e: unknown) {
if (e instanceof Prisma.PrismaClientInitializationError) {
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
} else {
throw e;
}
}
return [];
};
export default WithLayout({ getLayout: null, Page, getData });
export const dynamic = "force-static";
@@ -1,34 +0,0 @@
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { AppCategories } from "@calcom/prisma/enums";
import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check";
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
import CategoryPage, { type PageProps } from "~/apps/categories/[category]/category-view";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("app_store"),
(t) => t("app_store_description")
);
};
export const generateStaticParams = async () => {
const paths = Object.keys(AppCategories);
const isPrismaAvailable = await isPrismaAvailableCheck();
if (!isPrismaAvailable) {
// Database is not available at build time. Make sure we fall back to building these pages on demand
return [];
}
return paths.map((category) => ({ category }));
};
const getData = withAppDirSsg<PageProps>(getStaticProps, "future/apps/categories/[category]");
export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">;
export const dynamic = "force-static";
@@ -1,18 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSidePropsAppDir } from "@lib/apps/installed/[category]/getServerSideProps";
import Page from "~/apps/installed/[category]/installed-category-view";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("installed_apps"),
(t) => t("manage_your_connected_apps")
);
};
const getData = withAppDirSsr(getServerSidePropsAppDir);
export default WithLayout({ getLayout: null, getData, Page });
@@ -1,10 +1,10 @@
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 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 });
@@ -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,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,11 +0,0 @@
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import type { PageProps } from "~/team/type-view";
import LegacyPage from "~/team/type-view";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
@@ -1,44 +0,0 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { 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 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 { 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<LegacyPageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -1,11 +0,0 @@
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
const getEmbedData = withEmbedSsrAppDir<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
-28
View File
@@ -1,28 +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 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 getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
})<"P">;
-17
View File
@@ -1,17 +0,0 @@
"use client";
import { type NextPage } from "next";
import CustomError, { type DefaultErrorProps } from "./error";
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
return (
<html>
<body>
<CustomError {...props} />
</body>
</html>
);
};
export default GlobalError;
+240 -6
View File
@@ -1,12 +1,246 @@
import React from "react";
import { _generateMetadata, getTranslate } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { headers } from "next/headers";
import Link from "next/link";
import {
getOrgDomainConfigFromHostname,
subdomainSuffix,
} from "@calcom/features/ee/organizations/lib/orgDomains";
import { DOCS_URL, IS_CALCOM, WEBSITE_URL } from "@calcom/lib/constants";
import { Icon } from "@calcom/ui";
enum PageType {
ORG = "ORG",
TEAM = "TEAM",
USER = "USER",
OTHER = "OTHER",
}
function getPageInfo(pathname: string, host: string) {
const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ hostname: host });
const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
if (!routerUsername || (isValidOrgDomain && currentOrgDomain)) {
return {
username: currentOrgDomain ?? "",
pageType: PageType.ORG,
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
currentOrgDomain?.replace("/", "") ?? ""
}`,
};
}
const splitPath = routerUsername.split("/");
if (splitPath[1] === "team" && splitPath.length === 3) {
return {
username: splitPath[2],
pageType: PageType.TEAM,
url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`,
};
}
return {
username: routerUsername,
pageType: PageType.USER,
url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`,
};
}
async function NotFound() {
const t = await getTranslate();
const headersList = headers();
const host = headersList.get("x-forwarded-host") ?? "";
const pathname = headersList.get("x-pathname") ?? "";
// This makes more sense after full migration to App Router
// if (!pathname) {
// redirect("/500");
// }
const { username, pageType, url } = getPageInfo(pathname, host);
const isBookingSuccessPage = pathname?.startsWith("/booking");
const isSubpage = pathname?.includes("/", 2) || isBookingSuccessPage;
const isInsights = pathname?.startsWith("/insights");
const links = [
{
title: t("enterprise"),
description: "Learn more about organizations and subdomains in our enterprise plan.",
icon: "shield" as const,
href: `${WEBSITE_URL}/enterprise`,
},
{
title: t("documentation"),
description: t("documentation_description"),
icon: "file-text" as const,
href: DOCS_URL,
},
{
title: t("blog"),
description: t("blog_description"),
icon: "book-open" as const,
href: `${WEBSITE_URL}/blog`,
},
];
/**
* If we're on 404 and the route is insights it means it is disabled
* TODO: Abstract this for all disabled features
**/
if (isInsights) {
return (
<div className="min-h-screen bg-white px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
{t("feature_currently_disabled") ?? "Feature is currently disabled"}
</h1>
</div>
<div className="mt-12">
<div className="mt-8">
<Link href={WEBSITE_URL} className="text-base font-medium text-black hover:text-gray-500">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</main>
</div>
);
}
const NotFound = () => {
return (
<div data-testid="404-page">
<h1>404 - Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
<div className="bg-default min-h-screen px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{t("error_404")}</p>
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
{isBookingSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
</h1>
{isSubpage && pageType !== PageType.TEAM ? (
<span className="mt-2 inline-block text-lg">{t("check_spelling_mistakes_or_go_back")}</span>
) : IS_CALCOM ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t(`404_the_${pageType.toLowerCase()}`)}
{username ? (
<>
<strong className="text-blue-500">{username}</strong> {t("is_still_available")}{" "}
<span className="text-blue-500">{t("register_now")}</span>.
</>
) : null}
</a>
) : (
<span className="mt-2 inline-block text-lg">
{t(`404_the_${pageType.toLowerCase()}`)}{" "}
{username ? (
<>
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
{t("is_still_available")}
</>
) : null}
</span>
)}
</div>
<div className="mt-12">
{((!isSubpage && IS_CALCOM) || pageType === PageType.ORG || pageType === PageType.TEAM) && (
<ul role="list" className="my-4">
<li className="border-2 border-green-500 px-4 py-2">
<a
href={url}
target="_blank"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"
rel="noreferrer">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<Icon name="check" className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("register")}{" "}
<strong className="text-green-500">{`${
pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
</span>
</span>
</h3>
<p className="text-subtle text-base">{t(`404_claim_entity_${pageType.toLowerCase()}`)}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
)}
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">{t("popular_pages")}</h2>
<ul role="list" className="border-subtle divide-subtle divide-y">
{links
.filter((_, idx) => pageType === PageType.ORG || idx !== 0)
.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2">
<a
href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<Icon name={link.icon} className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{link.title}
</span>
</h3>
<p className="text-subtle text-base">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon name="chevron-right" className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
))}
</ul>
<div className="mt-8">
<Link href={WEBSITE_URL} className="hover:text-subtle text-emphasis text-base font-medium">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</main>
</div>
);
}
export const generateMetadata = async () => {
const headersList = headers();
const pathname = headersList.get("x-pathname") ?? "";
const isInsights = pathname?.startsWith("/insights");
const metadata = await _generateMetadata(
(t) =>
isInsights
? t("feature_currently_disabled") ?? "Feature is currently disabled"
: t("404_page_not_found"),
(t) => t("404_page_not_found")
);
return {
...metadata,
robots: {
index: false,
follow: false,
},
};
};
export default NotFound;
export default WithLayout({
ServerPage: NotFound,
});
@@ -0,0 +1,64 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
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;
const title = eventData?.title ?? "";
const meeting = {
title,
profile: { name: profileName, image: profileImage },
users: [
...(eventData?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
};
const metadata = await generateMeetingMetadata(
meeting,
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`,
isBrandingHidden,
getOrgFullOrigin(eventData?.entity.orgSlug ?? null)
);
return {
...metadata,
robots: {
follow: !(eventData?.hidden || !isSEOIndexable),
index: !(eventData?.hidden || !isSEOIndexable),
},
};
};
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 });
+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";
@@ -10,10 +10,10 @@ const paramsSchema = z
pages: [],
});
const Page = ({ params, searchParams }: PageProps) => {
const { pages } = paramsSchema.parse({ ...params, ...searchParams });
const Page = ({ params }: PageProps) => {
const { pages } = paramsSchema.parse(params);
redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
redirect(`/routing/${pages.length ? pages.join("/") : ""}`);
};
export default Page;
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
const Page = () => {
redirect("/apps/routing-forms/forms");
const Page = async () => {
redirect("/routing/forms");
};
export default Page;
+70
View File
@@ -0,0 +1,70 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsResult } from "next";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import z from "zod";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/routing/[...pages]/getServerSideProps";
import RoutingFormsPage, { getLayout } from "~/routing/[...pages]/pages-view";
const paramsSchema = z.object({
pages: z.array(z.string()),
});
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const ctx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const data = await getData({
...ctx,
params: {
slug: "routing-forms",
pages: params.pages,
},
});
const form = "form" in data ? (data.form as { name?: string; description?: string }) : null;
const formName = form?.name;
const formDescription = form?.description;
const { pages } = p.data;
if (pages.includes("routing-link")) {
return await _generateMetadata(
() => `${formName} | Cal.com Forms`,
() => "",
true
);
}
return await _generateMetadata(
(t) => formName ?? t("routing_forms"),
(t) => formDescription ?? t("routing_forms_description")
);
};
const getData = withAppDirSsr<GetServerSidePropsResult<any>>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
const ctx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData({
...ctx,
params: {
slug: "routing-forms",
pages: params.pages,
},
});
return <RoutingFormsPage {...props} />;
};
export default WithLayout({
getLayout,
ServerPage,
});
+7
View File
@@ -0,0 +1,7 @@
import { redirect } from "next/navigation";
const Page = async () => {
redirect("/routing/forms");
};
export default Page;
+58
View File
@@ -0,0 +1,58 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import LegacyPage from "~/team/type-view";
import 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 { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
const profileName = eventData?.profile?.name ?? "";
const profileImage = eventData?.profile.image;
const title = eventData?.title ?? "";
const meeting = {
title,
profile: { name: profileName, image: profileImage },
users: [
...(eventData?.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
};
const metadata = await generateMeetingMetadata(
meeting,
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${booking?.uid ? t("reschedule") : ""} ${title}`,
isBrandingHidden,
getOrgFullOrigin(eventData.entity.orgSlug ?? null)
);
return {
...metadata,
robots: {
follow: !(eventData?.hidden || !isSEOIndexable),
index: !(eventData?.hidden || !isSEOIndexable),
},
};
};
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
+52
View File
@@ -0,0 +1,52 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps as _PageProps } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
import type { PageProps } from "~/team/team-view";
import LegacyPage from "~/team/team-view";
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams)
);
const meeting = {
title: markdownStrippedBio ?? "",
profile: {
name: `${team.name}`,
image: getOrgOrTeamAvatar(team),
},
};
const metadata = await generateMeetingMetadata(
meeting,
(t) => team.name || t("nameless_team"),
(t) => team.name || t("nameless_team"),
false,
getOrgFullOrigin(currentOrgDomain ?? null)
);
return {
...metadata,
robots: {
follow: isSEOIndexable,
index: isSEOIndexable,
},
};
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
-6
View File
@@ -1,7 +1,6 @@
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import type { AppPageProps } from "./AppPage";
import { AppPage } from "./AppPage";
@@ -14,11 +13,6 @@ const ShellHeading = () => {
export default function WrappedApp(props: AppPageProps) {
return (
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
<HeadSeo
title={props.name}
description={props.description}
app={{ slug: props.logo, name: props.name, description: props.description }}
/>
{props.licenseRequired ? (
<LicenseRequired>
<AppPage {...props} />
+14 -14
View File
@@ -511,9 +511,9 @@ function BookingListItem(booking: BookingItemProps) {
</DialogFooter>
</DialogContent>
</Dialog>
<tr data-testid="booking-item" className="hover:bg-muted group transition ">
<div data-testid="booking-item" className="hover:bg-muted group w-full">
<div className="flex flex-col sm:flex-row">
<td className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
<div className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
<div className="flex h-full items-center">
{eventTypeColor && (
<div className="h-[70%] w-0.5" style={{ backgroundColor: eventTypeColor }} />
@@ -562,8 +562,10 @@ function BookingListItem(booking: BookingItemProps) {
</div>
</Link>
</div>
</td>
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
</div>
<div
data-testid="title-and-attendees"
className={`w-full px-4${isRejected ? " line-through" : ""}`}>
<Link href={bookingLink}>
{/* Time and Badges for mobile */}
<div className="w-full pb-2 pt-4 sm:hidden">
@@ -648,8 +650,8 @@ function BookingListItem(booking: BookingItemProps) {
)}
</div>
</Link>
</td>
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
</div>
<div className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && <TableActions actions={pendingActions} />}
@@ -674,7 +676,7 @@ function BookingListItem(booking: BookingItemProps) {
<TableActions actions={chargeCardActions} />
</div>
)}
</td>
</div>
</div>
<BookingItemBadges
booking={booking}
@@ -683,7 +685,7 @@ function BookingListItem(booking: BookingItemProps) {
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
/>
</tr>
</div>
{isBookingReroutable(parsedBooking) && (
<RerouteDialog
@@ -712,7 +714,7 @@ const BookingItemBadges = ({
const { t } = useLocale();
return (
<div className="hidden h-9 flex-row pb-4 pl-6 sm:flex">
<div className="hidden h-9 flex-row items-center pb-4 pl-6 sm:flex">
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("unconfirmed")}
@@ -1251,11 +1253,9 @@ const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: Assig
const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum);
return (
<Tooltip content={<p>{assignmentReason.reasonString}</p>}>
<div className="-mt-1">
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
{t(badgeTitle)}
</Badge>
</div>
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
{t(badgeTitle)}
</Badge>
</Tooltip>
);
};
+1 -1
View File
@@ -807,7 +807,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({
<div>
<Link
className="text-emphasis text-semibold font-cal mb-4 flex underline"
href={`/apps/routing-forms/form-edit/${form.id}`}
href={`/routing/form-edit/${form.id}`}
target="_blank">
{form.name}
</Link>
+31 -12
View File
@@ -1,6 +1,9 @@
"use client";
import React from "react";
import { HttpError } from "@calcom/lib/http-error";
import { Button } from "@calcom/ui";
type Props = {
statusCode?: number | null;
@@ -53,21 +56,37 @@ export const ErrorPage: React.FC<Props> = (props) => {
return (
<>
<div className="bg-default min-h-screen px-4">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
<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">{statusCode}</h1>
<h2 className="text-emphasis mt-6 max-w-2xl 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">
Something went wrong on our end. Get in touch with our support team, and we&apos;ll get it fixed
right away for you.
</p>
<div className="mb-8 flex flex-col">
<p className="text-default mb-4 max-w-2xl text-sm">
Please provide the following text when contacting support to better help you:
</p>
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{message}
</h1>
</pre>
</div>
</main>
{displayDebug && (
<div className="flex-wrap">
<ErrorDebugPanel error={error} />
</div>
)}
<Button href="mailto:support@cal.com">Contact Support</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back
</Button>
</div>
</div>
{displayDebug && (
<div className="flex-wrap">
<ErrorDebugPanel error={error} />
</div>
)}
</>
);
};
@@ -2,7 +2,6 @@ import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
@@ -32,7 +31,6 @@ type NotFound = {
// TODO: It is a candidate for apps.*.generated.*
const AppsRouting = {
"routing-forms": RoutingFormsRoutingConfig,
typeform: TypeformRoutingConfig,
};
+11 -19
View File
@@ -1,7 +1,5 @@
import fs from "fs";
import matter from "gray-matter";
import MarkdownIt from "markdown-it";
import type { GetStaticPropsContext } from "next";
import path from "path";
import { z } from "zod";
@@ -10,8 +8,6 @@ import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
const md = new MarkdownIt("default", { html: true, breaks: true });
export const sourceSchema = z.object({
content: z.string(),
data: z.object({
@@ -29,15 +25,15 @@ export const sourceSchema = z.object({
}),
});
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
export type AppDataProps = NonNullable<Awaited<ReturnType<typeof getStaticProps>>>;
export const getStaticProps = async (slug: string) => {
const appMeta = await getAppWithMetadata({
slug: ctx.params?.slug,
slug,
});
const appFromDb = await prisma.app.findUnique({
where: { slug: ctx.params.slug.toLowerCase() },
where: { slug: slug.toLowerCase() },
});
const isAppAvailableInFileSystem = appMeta;
@@ -45,16 +41,14 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (!IS_PRODUCTION && isAppDisabled) {
return {
props: {
isAppDisabled: true as const,
data: {
...appMeta,
},
isAppDisabled: true as const,
data: {
...appMeta,
},
};
}
if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true } as const;
if (!appFromDb || !appMeta || isAppDisabled) return null;
const isTemplate = appMeta.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
@@ -85,10 +79,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
});
}
return {
props: {
isAppDisabled: false as const,
source: { content, data },
data: appMeta,
},
isAppDisabled: false as const,
source: { content, data },
data: appMeta,
};
};
@@ -1,12 +1,10 @@
import type { GetStaticPropsContext } from "next";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import prisma from "@calcom/prisma";
import type { AppCategories } from "@calcom/prisma/enums";
export const getStaticProps = async (context: GetStaticPropsContext) => {
const category = context.params?.category as AppCategories;
export type CategoryDataProps = NonNullable<Awaited<ReturnType<typeof getStaticProps>>>;
export const getStaticProps = async (category: AppCategories) => {
const appQuery = await prisma.app.findMany({
where: {
categories: {
@@ -24,8 +22,7 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
return {
props: {
apps,
},
apps,
category,
};
};
@@ -1,78 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { AppCategories } from "@calcom/prisma/enums";
export type querySchemaType = z.infer<typeof querySchema>;
export const querySchema = z.object({
category: z.nativeEnum(AppCategories),
});
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
// get return-to cookie and redirect if needed
const { cookies } = ctx.req;
const returnTo = cookies["return-to"];
if (cookies && returnTo) {
ctx.res.setHeader("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
const redirect = {
redirect: {
destination: `${returnTo}`,
permanent: false,
},
} as const;
return redirect;
}
const params = querySchema.safeParse(ctx.params);
if (!params.success) {
const notFound = { notFound: true } as const;
return notFound;
}
return {
props: {
category: params.data.category,
},
};
}
export async function getServerSidePropsAppDir(ctx: GetServerSidePropsContext) {
// get return-to cookie and redirect if needed
const { cookies } = ctx.req;
const returnTo = cookies["return-to"];
if (cookies && returnTo) {
const NextResponse = await import("next/server").then((mod) => mod.NextResponse);
const response = NextResponse.next();
response.headers.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
const redirect = {
redirect: {
destination: `${returnTo}`,
permanent: false,
},
} as const;
return redirect;
}
const params = querySchema.safeParse(ctx.params);
if (!params.success) {
const notFound = { notFound: true } as const;
return notFound;
}
return {
props: {
category: params.data.category,
},
};
}
@@ -1,3 +0,0 @@
export async function getServerSideProps() {
return { redirect: { permanent: false, destination: "/apps/installed/calendar" } };
}
@@ -121,6 +121,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return {
props: {
eventData,
entity: eventData.entity,
duration: getMultipleDurationValue(
eventData.metadata?.multipleDuration,
@@ -0,0 +1,118 @@
import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing-server.config";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { prisma } from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
import { ssrInit } from "@server/lib/ssr";
type RoutingPageType = {
getServerSideProps?: AppGetServerSideProps;
// A component than can accept any properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ((props: any) => JSX.Element) &
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
};
type Found = {
notFound: false;
getServerSideProps: RoutingPageType["getServerSideProps"];
};
type NotFound = {
notFound: true;
};
function getRoute(pages: string[]) {
const mainPage = pages[0];
const getServerSideProps = routingServerSidePropsConfig[mainPage] as RoutingPageType["getServerSideProps"];
if (!getServerSideProps || typeof getServerSideProps !== "function") {
return {
notFound: true,
} as NotFound;
}
return { notFound: false, getServerSideProps } as Found;
}
const paramsSchema = z.object({
slug: z.string(),
pages: z.array(z.string()),
});
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<any>> {
const { params, req } = context;
if (!params) {
return {
notFound: true,
};
}
const parsedParams = paramsSchema.safeParse(params);
if (!parsedParams.success) {
return {
notFound: true,
};
}
const appName = parsedParams.data.slug;
const pages = parsedParams.data.pages;
const route = getRoute(pages);
if (route.notFound) {
return { notFound: true };
}
if (route.getServerSideProps) {
params.appPages = pages.slice(1);
const session = await getServerSession({ req });
const user = session?.user;
const app = await getAppWithMetadata({ slug: appName });
if (!app) {
return {
notFound: true,
};
}
const result = await route.getServerSideProps(
context as GetServerSidePropsContext<{
slug: string;
pages: string[];
appPages: string[];
}>,
prisma,
user,
ssrInit
);
if (result.notFound) {
return { notFound: true };
}
if (result.redirect) {
return { redirect: result.redirect };
}
return {
props: {
appName,
appUrl: app.simplePath,
...result.props,
},
};
} else {
return {
props: {
appName,
},
};
}
}
+25 -29
View File
@@ -22,7 +22,6 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const url = req.nextUrl;
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-url", req.url);
if (!url.pathname.startsWith("/api")) {
@@ -64,11 +63,9 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
requestHeaders.set("x-csp-enforce", "true");
}
if (url.pathname.startsWith("/future/apps/installed")) {
if (url.pathname.startsWith("/apps/installed")) {
const returnTo = req.cookies.get("return-to")?.value;
if (returnTo !== undefined) {
requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
let validPathname = returnTo;
try {
@@ -77,8 +74,14 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const nextUrl = url.clone();
nextUrl.pathname = validPathname;
// TODO: Consider using responseWithHeaders here
return NextResponse.redirect(nextUrl, { headers: requestHeaders });
const response = NextResponse.redirect(nextUrl);
response.cookies.set("return-to", "", {
expires: new Date(0),
path: "/",
});
return response;
}
}
@@ -108,9 +111,10 @@ const routingForms = {
handleRewrite: (url: URL) => {
// Don't 404 old routing_forms links
if (url.pathname.startsWith("/apps/routing_forms")) {
url.pathname = url.pathname.replace(/^\/apps\/routing_forms($|\/)/, "/apps/routing-forms/");
url.pathname = url.pathname.replace(/^\/apps\/routing_forms($|\/)/, "/routing/");
return NextResponse.rewrite(url);
}
return null;
},
};
@@ -155,6 +159,7 @@ export const config = {
matcher: [
"/403",
"/500",
"/icons",
"/d/:path*",
"/more/:path*",
"/maintenance/:path*",
@@ -167,34 +172,25 @@ export const config = {
"/api/trpc/:path*",
"/login",
"/auth/login",
"/future/auth/login",
/**
* Paths required by routingForms.handle
*/
"/apps/routing_forms/:path*",
"/event-types/:path*",
"/apps/installed/:category/",
"/future/apps/installed/:category/",
"/apps/:slug/",
"/future/apps/:slug/",
"/apps/:slug/setup/",
"/future/apps/:slug/setup/",
"/apps/categories/",
"/future/apps/categories/",
"/apps/categories/:category/",
"/future/apps/categories/:category/",
"/workflows/:path*",
"/apps/:path*",
"/getting-started/:path*",
"/apps",
"/bookings/:status/",
"/workflows/:path*",
"/event-types/:path*",
"/routing/:path*",
"/bookings/:path*",
"/video/:path*",
"/teams",
"/future/teams/",
"/teams/:path*",
"/signup/:path*",
"/settings/:path*",
"/reschedule/:path*",
"/availability/:path*",
"/booking/:path*",
"/routing-forms/:path*",
"/team/:path*",
"/org/[orgSlug]/[user]/[type]",
"/org/[orgSlug]/team/[slug]/[type]",
"/org/[orgSlug]/team/[slug]",
"/org/[orgSlug]",
],
};
@@ -1,6 +1,5 @@
"use client";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
@@ -29,7 +28,6 @@ type NotFound = {
// TODO: It is a candidate for apps.*.generated.*
const AppsRouting = {
"routing-forms": RoutingFormsRoutingConfig,
typeform: TypeformRoutingConfig,
};
@@ -7,7 +7,6 @@ import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import type { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { HeadSeo } from "@calcom/ui";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
@@ -28,11 +27,5 @@ export default function SetupInformation(props: PageProps) {
router.replace(`/auth/login?${urlSearchParams.toString()}`);
}
return (
<>
{/* So that the set up page does not get indexed by search engines */}
<HeadSeo nextSeoProps={{ noindex: true, nofollow: true }} title={`${slug} | Cal.com`} description="" />
<AppSetupPage slug={slug} {...props} />
</>
);
return <AppSetupPage slug={slug} {...props} />;
}
+2 -8
View File
@@ -1,7 +1,5 @@
"use client";
import MarkdownIt from "markdown-it";
import type { InferGetStaticPropsType } from "next";
import Link from "next/link";
import { IS_PRODUCTION } from "@calcom/lib/constants";
@@ -9,16 +7,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { showToast } from "@calcom/ui";
import type { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
import type { AppDataProps } from "@lib/apps/[slug]/getStaticProps";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import App from "@components/apps/App";
const md = new MarkdownIt("default", { html: true, breaks: true });
export type PageProps = InferGetStaticPropsType<typeof getStaticProps>;
function SingleAppPage(props: PageProps) {
function SingleAppPage(props: AppDataProps) {
const { error, setQuery: setError } = useRouterQuery("error");
const { t } = useLocale();
if (error === "account_already_linked") {
@@ -1,20 +1,15 @@
"use client";
import type { InferGetStaticPropsType } from "next";
import Link from "next/link";
import Shell from "@calcom/features/shell/Shell";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCard, SkeletonText } from "@calcom/ui";
import type { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
import type { CategoryDataProps } from "@lib/apps/categories/[category]/getStaticProps";
export type PageProps = InferGetStaticPropsType<typeof getStaticProps>;
export default function Apps({ apps }: PageProps) {
const searchParams = useCompatSearchParams();
export default function Apps({ apps, category }: CategoryDataProps) {
const { t, isLocaleReady } = useLocale();
const category = searchParams?.get("category");
return (
<>
@@ -1,6 +1,5 @@
"use client";
import Head from "next/head";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
@@ -226,12 +225,6 @@ const OnboardingPage = ({
key={pathname}
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen px-4"
data-testid="onboarding">
<Head>
<title>
{t("install")} {appMetadata?.name ?? ""}
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="mx-auto py-6 sm:px-4 md:py-24">
<div className="relative">
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]" ref={formPortalRef}>
@@ -8,11 +8,9 @@ import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/a
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import type { RemoveAppParams } from "@calcom/features/apps/components/DisconnectIntegrationModal";
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCategories } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { Icon } from "@calcom/ui";
import {
AppSkeletonLoader as SkeletonLoader,
@@ -23,7 +21,6 @@ import {
} from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import type { querySchemaType, getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps";
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
@@ -183,13 +180,13 @@ type ModalState = {
teamId?: number;
};
export type PageProps = inferSSRProps<typeof getServerSideProps>;
type PageProps = {
category: AppCategories;
};
export default function InstalledApps(props: PageProps) {
const searchParams = useCompatSearchParams();
export default function InstalledApps({ category }: PageProps) {
const { t } = useLocale();
const utils = trpc.useUtils();
const category = searchParams?.get("category") as querySchemaType["category"];
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
// Exclude calendar and other from categoryList, we handle those slightly differently below
return !(category in { other: null, calendar: null });
@@ -1,8 +1,15 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Fragment, ReactElement, useState } from "react";
import { z } from "zod";
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
createColumnHelper,
} from "@tanstack/react-table";
import type { ReactElement } from "react";
import { Fragment, useMemo, useState } from "react";
import type { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import dayjs from "@calcom/dayjs";
@@ -10,20 +17,20 @@ import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle"
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
import Shell from "@calcom/features/shell/Shell";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
import { Alert, EmptyScreen, HorizontalTabs } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { validStatuses } from "~/bookings/lib/validStatuses";
import type { validStatuses } from "~/bookings/lib/validStatuses";
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
@@ -66,7 +73,24 @@ const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
unconfirmed: "unconfirmed_bookings",
};
export default function Bookings({ status }: { status: (typeof validStatuses)[number] }) {
type BookingsProps = {
status: (typeof validStatuses)[number];
};
export default function Bookings(props: BookingsProps) {
return (
<DataTableProvider>
<BookingsContent {...props} />
</DataTableProvider>
);
}
type RowData = {
booking: BookingOutput;
recurringInfo?: RecurringInfo;
};
function BookingsContent({ status }: BookingsProps) {
const { data: filterQuery } = useFilterQuery();
const { t } = useLocale();
@@ -87,13 +111,32 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
}
);
// Animate page (tab) transitions to look smoothing
const columns = useMemo(() => {
const columnHelper = createColumnHelper<RowData>();
const buttonInView = useInViewObserver(() => {
if (!query.isFetching && query.hasNextPage && query.status === "success") {
query.fetchNextPage();
}
});
return [
columnHelper.display({
id: "custom-view",
cell: (props) => {
const { booking, recurringInfo } = props.row.original;
return (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
},
}),
];
}, [user, status]);
const isEmpty = !query.data?.pages[0]?.bookings.length;
@@ -120,6 +163,27 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
return true;
};
const flatData = useMemo(() => {
return (
query.data?.pages.flatMap((page) =>
page.bookings.filter(filterBookings).map((booking) => ({
booking,
recurringInfo: page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
),
}))
) || []
);
}, [query.data]);
const table = useReactTable<RowData>({
data: flatData,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
});
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
@@ -136,8 +200,6 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
@@ -146,7 +208,7 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
<div className="flex w-full flex-col">
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
@@ -158,69 +220,41 @@ export default function Bookings({ status }: { status: (typeof validStatuses)[nu
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
<div
className="bg-default divide-subtle w-full max-w-full divide-y"
data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</div>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
loggedInUser={{
userId: user?.id,
userTimeZone: user?.timeZone,
userTimeFormat: user?.timeFormat,
userEmail: user?.email,
}}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
<DataTableWrapper
table={table}
testId={`${status}-bookings`}
bodyTestId="bookings"
hideHeader={true}
isPending={query.isFetching && !flatData}
hasNextPage={query.hasNextPage}
fetchNextPage={query.fetchNextPage}
isFetching={query.isFetching}
variant="compact"
/>
</>
)}
{query.status === "success" && isEmpty && (
@@ -0,0 +1,76 @@
"use client";
import { useParams } from "next/navigation";
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { AppProps } from "@lib/app-providers";
import type { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
type RoutingPageType = {
getServerSideProps: AppGetServerSideProps;
// A component than can accept any properties
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: ((props: any) => JSX.Element) &
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
};
type Found = {
notFound: false;
Component: RoutingPageType["default"];
getServerSideProps: RoutingPageType["getServerSideProps"];
};
type NotFound = {
notFound: true;
};
function getRoute(pages: string[]) {
const routingConfig = RoutingFormsRoutingConfig as unknown as Record<string, RoutingPageType>;
const mainPage = pages[0];
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as RoutingPageType);
if (!appPage) {
return {
notFound: true,
} as NotFound;
}
return { notFound: false, Component: appPage.default, ...appPage } as Found;
}
export type PageProps = inferSSRProps<typeof getServerSideProps>;
const RoutingFormsPage: RoutingPageType["default"] = function RoutingFormsPage(props: PageProps) {
const params = useParams();
const _pages = params?.pages ?? [];
const pages = Array.isArray(_pages) ? _pages : _pages.split("/");
const route = getRoute(pages);
const componentProps = {
...props,
pages: pages.slice(1),
};
if (!route || route.notFound) {
throw new Error("Route can't be undefined");
}
return <route.Component {...componentProps} />;
};
export const getLayout: NonNullable<(typeof RoutingFormsPage)["getLayout"]> = (page) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const params = useParams();
const _pages = params?.pages ?? [];
const pages = Array.isArray(_pages) ? _pages : _pages.split("/");
const route = getRoute(pages as string[]);
if (route.notFound) {
return null;
}
if (!route.Component.getLayout) {
return page;
}
return route.Component.getLayout(page);
};
export default RoutingFormsPage;
+1 -26
View File
@@ -5,7 +5,6 @@ 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";
@@ -26,7 +25,6 @@ function Type({
slug,
user,
booking,
isEmbed,
isBrandingHidden,
eventData,
isInstantMeeting,
@@ -34,33 +32,12 @@ function Type({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isSEOIndexable,
isEmbed,
}: 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}
@@ -87,6 +64,4 @@ function Type({
);
}
Type.isBookingPage = true;
export default Type;
+1 -1
View File
@@ -295,7 +295,7 @@ const nextConfig = {
const beforeFiles = [
{
source: "/forms/:formQuery*",
destination: "/apps/routing-forms/routing-link/:formQuery*",
destination: "/routing/routing-link/:formQuery*",
},
{
source: "/success/:path*",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.8.16",
"version": "4.8.19",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+269 -26
View File
@@ -1,18 +1,63 @@
/* eslint-disable import/no-anonymous-default-export */
import type { Card } from "@pages/api/plain";
import dayjs from "@calcom/dayjs";
export default (
name: string,
email: string,
id: string,
username: string,
timeZone: string,
emailVerified: Date | null,
twoFactorEnabled: boolean | null
twoFactorEnabled: boolean | null,
identityProvider: string | null,
lastActiveAt: Date | null,
teamName: string | null,
teamSlug: string | null,
isOrganization: boolean | null,
stripeCustomerId: string | null
): Card => {
return {
key: "customer-card",
timeToLiveSeconds: null,
components: [
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentText: {
text: "Basic Information",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Name",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: name || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
@@ -42,31 +87,6 @@ export default (
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email Verified?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
@@ -115,6 +135,24 @@ export default (
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
// Account Settings Section Title
{
componentText: {
text: "Account Settings",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
@@ -139,6 +177,211 @@ export default (
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Identity Provider",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: identityProvider || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Last Active At",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: lastActiveAt ? dayjs(lastActiveAt).fromNow() : "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
// Team Information Section Title
{
componentText: {
text: "Team Information",
textColor: "NORMAL",
textSize: "L",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Team Name",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: teamName || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Team Slug",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: teamSlug || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Is Organization",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: isOrganization ? "Yes" : "No",
badgeColor: isOrganization ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentDivider: {
dividerSpacingSize: "M",
},
},
{
componentText: {
text: "Security & Billing",
textColor: "NORMAL",
textSize: "M",
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Stripe Customer ID",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentText: {
text: stripeCustomerId || "Unknown",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: "Email Verified?",
textColor: "MUTED",
},
},
],
rowAsideContent: [
{
componentBadge: {
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
},
},
],
},
},
{
componentSpacer: {
spacerSize: "M",
},
},
{
componentRow: {
rowMainContent: [
@@ -2,10 +2,17 @@ import type { Card } from "@pages/api/plain";
import customerCardDisplay from "@pages/api/plain/customer-card-display";
export const cardExamples: ((
name: string,
email: string,
id: string,
username: string,
timeZone: string,
emailVerified: Date | null,
twoFactorEnabled: boolean | null
twoFactorEnabled: boolean | null,
identityProvider: string | null,
lastActiveAt: Date | null,
teamName: string | null,
teamSlug: string | null,
isOrganization: boolean | null,
stripeCustomerId: string | null
) => Card)[] = [customerCardDisplay];
-15
View File
@@ -1,15 +0,0 @@
import { getServerSideProps } from "@lib/apps/[slug]/[...pages]/getServerSideProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/[slug]/[...pages]/pages-view";
import PagesView, { getLayout } from "~/apps/[slug]/[...pages]/pages-view";
const Page = (props: PageProps) => <PagesView {...props} />;
Page.PageWrapper = PageWrapper;
Page.getLayout = getLayout;
export { getServerSideProps };
export default Page;
-39
View File
@@ -1,39 +0,0 @@
import { Prisma } from "@prisma/client";
import type { GetStaticPaths } from "next";
import { AppRepository } from "@calcom/lib/server/repository/app";
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/[slug]/slug-view";
import SingleAppPage from "~/apps/[slug]/slug-view";
const Page = (props: PageProps) => <SingleAppPage {...props} />;
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
let paths: { params: { slug: string } }[] = [];
try {
const appStore = await AppRepository.findAppStore();
paths = appStore.map(({ slug }) => ({ params: { slug } }));
} catch (e: unknown) {
if (e instanceof Prisma.PrismaClientInitializationError) {
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
} else {
throw e;
}
}
return {
paths,
fallback: "blocking",
};
};
export { getStaticProps };
Page.PageWrapper = PageWrapper;
export default Page;
-14
View File
@@ -1,14 +0,0 @@
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/[slug]/setup/setup-view";
import SetupView from "~/apps/[slug]/setup/setup-view";
const Page = (props: PageProps) => <SetupView {...props} />;
Page.PageWrapper = PageWrapper;
export { getServerSideProps };
export default Page;
@@ -1,33 +0,0 @@
import { AppCategories } from "@calcom/prisma/enums";
import { isPrismaAvailableCheck } from "@calcom/prisma/is-prisma-available-check";
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/categories/[category]/category-view";
import CategoryView from "~/apps/categories/[category]/category-view";
const Page = (props: PageProps) => <CategoryView {...props} />;
Page.PageWrapper = PageWrapper;
export const getStaticPaths = async () => {
const paths = Object.keys(AppCategories);
const isPrismaAvailable = await isPrismaAvailableCheck();
if (!isPrismaAvailable) {
// Database is not available at build time. Make sure we fall back to building these pages on demand
return {
paths: [],
fallback: "blocking",
};
}
return {
paths: paths.map((category) => ({ params: { category } })),
fallback: false,
};
};
export default Page;
export { getStaticProps };
-12
View File
@@ -1,12 +0,0 @@
import { getServerSideProps } from "@lib/apps/categories/getServerSideProps";
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/categories/categories-view";
import Apps from "~/apps/categories/categories-view";
const Page = (props: PageProps) => <Apps {...props} />;
Page.PageWrapper = PageWrapper;
export default Page;
export { getServerSideProps };
@@ -1,12 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import type { OnboardingPageProps } from "~/apps/installation/[[...step]]/step-view";
import StepView from "~/apps/installation/[[...step]]/step-view";
const Page = (props: OnboardingPageProps) => <StepView {...props} />;
Page.PageWrapper = PageWrapper;
export { getServerSideProps } from "@lib/apps/installation/[[...step]]/getServerSideProps";
export default Page;
@@ -1,12 +0,0 @@
import PageWrapper from "@components/PageWrapper";
import type { PageProps } from "~/apps/installed/[category]/installed-category-view";
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
const Page = (props: PageProps) => <InstalledApps {...props} />;
Page.PageWrapper = PageWrapper;
export { getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps";
export default Page;
-7
View File
@@ -1,7 +0,0 @@
export { getServerSideProps } from "@lib/apps/installed/getServerSideProps";
function RedirectPage() {
return;
}
export default RedirectPage;
@@ -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;
+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 +0,0 @@
export { getServerSideProps, default } from "@pages/team/[slug]/[type]";
@@ -1 +0,0 @@
export { getServerSideProps, default } from "@pages/team/[slug]";
@@ -1,25 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import z from "zod";
const paramsSchema = z
.object({
pages: z.array(z.string()),
})
.catch({
pages: [],
});
export default function RoutingForms() {
return null;
}
export const getServerSideProps = (context: GetServerSidePropsContext) => {
const { pages } = paramsSchema.parse(context.params);
return {
redirect: {
destination: `/apps/routing-forms/${pages.length ? pages.join("/") : ""}`,
permanent: false,
},
};
};
-12
View File
@@ -1,12 +0,0 @@
export default function RoutingFormsIndex() {
return null;
}
export const getServerSideProps = () => {
return {
redirect: {
destination: `/apps/routing-forms/forms`,
permanent: false,
},
};
};
-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";
@@ -0,0 +1,15 @@
import { expectPageToBeNotFound } from "playwright/lib/testUtils";
import { test } from "./lib/fixtures";
test.describe("App Router - error handling", () => {
test("404s must render App Router's not-found page regardless of whether pages router or app router is used", async ({
page,
}) => {
await expectPageToBeNotFound({ page, url: "/123491234" });
await expectPageToBeNotFound({ page, url: "/team/123491234" });
await expectPageToBeNotFound({ page, url: "/org/123491234" });
await expectPageToBeNotFound({ page, url: "/insights/123491234" });
await expectPageToBeNotFound({ page, url: "/login/123491234" });
});
});
@@ -31,6 +31,8 @@ function getOrgOrigin(orgSlug: string | null) {
return orgOrigin;
}
test.describe.configure({ mode: "parallel" });
test.describe("Bookings", () => {
test.afterEach(async ({ orgs, users, page }) => {
await users.deleteAll();
@@ -68,7 +68,18 @@ async function verifyRobotsMetaTag({ page, orgSlug, urls, expectedContent }: Ver
const { url } = await goToUrlWithErrorHandling(relativeUrl);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual(expectedContent);
expect(metaTagValue).not.toBeNull();
expect(
metaTagValue
?.split(",")
.map((s) => s.trim())
.join(",")
).toEqual(
expectedContent
.split(",")
.map((s) => s.trim())
.join(",")
);
}
});
}

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