Compare commits

...
69 Commits
Author SHA1 Message Date
Keith Williams 0860036607 v3.7.7
release.yml / release (push) Canceled after 0s
2024-01-24 11:06:38 -03:00
Peer RichelsenandOmar López d080a0ef2d feat: notion calendar (#13299)
* replaced cron with notion calendar

* fixed app store for cron

* fixed slug

* Apply suggestions from code review

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-24 12:10:15 +00:00
a6eab74fd1 feat: make org members list private (#13373)
* feat: make org members list private

* fix: add isOrg to MakeTeamPrivateSwitch in other-team-members-view

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-01-24 11:28:34 +00:00
0a2e275908 fix: UI mismatch with form state (#10651)
* fix: UI mismatch with form state

* Fix: Sync form state with ui for more fields & slight code refactor

* Ensure Stripe app is enabled to disable seats

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-01-24 10:55:31 +00:00
Crowdin Bot 3b05ebc5b5 New Crowdin translations by Github Action 2024-01-24 10:46:13 +00:00
Omar López e5b4d2fb80 fix: API slots endpoint return format (#13377) 2024-01-24 10:43:13 +00:00
Syed Ali Shahbaz 00a2a3d8a8 Fix slot wrapper (#13379) 2024-01-24 07:01:57 +00:00
30ba168fc3 chore: bump @trpc/* and @tanstack/react-query (#13335)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Gergő Móricz <mo.geryy@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-23 16:42:29 -07:00
SashaandDmytro Hryshyn 12eec186b2 chore: [app-router-migration 31] migrate the "/auth/setup" page (#13196)
* wip

* chore: setup page migration

* fix: conventions

* fix: convention

* use client

* fix: rebase on 13.5

---------

Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
2024-01-23 18:05:47 -03:00
MorganandOmar López 4417d649ea refactor: remove @calcom/features dependency to @calcom/web (#13365)
* refactor: remove @calcom/features dependency to @calcom/web

* refactor: remove IS_VISUAL_REGRESSION_TESTING from web

* fix: wrong imports

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-23 17:17:36 +00:00
Alex van Andel 7058da70e6 v3.7.6
release.yml / release (push) Canceled after 0s
2024-01-23 14:10:48 +00:00
Alex van Andel 2cb383c337 fix: Remove type from import getServerSideProps 2024-01-23 14:09:59 +00:00
Alex van Andel fe5eca8eb3 v3.7.5
release.yml / release (push) Canceled after 0s
2024-01-23 13:50:01 +00:00
Alex van Andel 7c6468953a fix: Update yarn.lock to include new apps 2024-01-23 13:48:15 +00:00
Alex van Andel 28a0553d18 v3.7.4
release.yml / release (push) Canceled after 0s
2024-01-23 13:42:40 +00:00
Alex van Andel ffcb04fa99 fix: Broken Cal.video due to missing export 2024-01-23 13:38:23 +00:00
Keith Williams 5ca216b922 v3.7.3
release.yml / release (push) Canceled after 0s
2024-01-23 09:34:58 -03:00
0abff9f562 feat: Add ICS Feed support (#13337)
* feat: ics-feed proof of concept pt. 1

* feat: ics-feedcalendar fixes

* fix: ics feed i18n and icon issues

* fix: remove debug log

* chore: revert yarn.lock

* fix: remove debug log

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-01-23 12:30:40 +00:00
Carina WollendorferandCarinaWolli 942dc38b58 fix: dark mode for OAuth /authorize page (#13363)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-22 21:40:03 +00:00
Benny Joo 827d8995a1 chore: [app-router-migration 38]: Refactor migrated pages and fix generateMetadata functions (#13314)
* refactor to keep consistencies in files

* manual: more refactors

* revert unnecessary changes

* fix type errors

* fix build error

* fix
2024-01-22 17:32:15 -03:00
850e222f0e feat: instant meeting improvements (#13258)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-01-22 19:48:23 +00:00
Mehul ede2357203 fix: SAML postgres err (#12437) 2024-01-22 15:41:43 +00:00
559ab4192f fix: team invite broken link (#13344)
* fix: team invite broken link

* default value fix

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-22 15:31:57 +00:00
3c8c6d0574 feat: Add Owner during organization creation (#13069)
* feat: Add Owner during organization creation

* chore: remove dead code

* fix: change route from onboard-admin to onboard-members

* fix: change button name to add_members from add_team_member

* fix: change MembershipRole badge colour

* fix: previous membership role bug

* update: "On-board administrators" tests

* fix: wrong import

* Reverted extra changes

* fix: current user cannot remove itself

* Reverted extra changes

* Reverted extra changes 2

* send isOrg to removeMember handler

* fix: removed prisma.user.delete cleanup as not needed

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-01-22 10:24:09 -05:00
Hariom Balhara 2ae2c15083 Validate that a user isnt a member of the organization before marking the username unavailable (#13354) 2024-01-22 08:01:55 -03:00
313bb89789 refactor: Booker Component (#13176)
* refactor: booker component

* refactor: booker component

* refactor: useBookings, useOverlayCalendar

* fix: imports and types

* fix: don't allow uid update in bookings update api (#13071)

* don't allow uid update in bookings update api

* omit uid from schemaBookingEditBodyParams instead of updating schemaBookingBaseBodyParams

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* New Crowdin translations by Github Action

* fix: use DateRangePicker component in my-account/out-of-office (#13207)

* fix: remove duplicate useBrandColors

* fix: add missing localstorage package

* fix: booking form initial values

* fix: redirect to payment form

* refactor: handle fetching schedule only in Booker

* fixup! refactor: handle fetching schedule only in Booker

* fix: remove empty file

* useBookerLayout.ts useEffect dependencies

isMobile and isEmbed were missing and validateLayout was redundant

* useInitialFormValues useEffect dependencies

* fix: remove unused hook

* refactor: split logic from data fetch overlayCalendar

* fix: small changes

* fix: inifinite loop

* fix: toggle calendar

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Riddhesh Mahajan <40472653+riddhesh-mahajan@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2024-01-22 11:57:00 +02:00
MehulandKeith Williams 95e037c664 fix: event api accepts invalid start & end values (#13203)
* fix: event api accepts invalid start & end values

* fix: unit tests

* test: added 'event length check during booking'

* test: fix unit test

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-20 17:31:11 +00:00
techknowlogick da026d909d feat(app-store): add feishu calendar (#13187) 2024-01-20 14:36:09 +00:00
Riddhesh Mahajan c6b50bfd14 prevent primary key update in memberships api (#13083) 2024-01-20 10:41:50 -03:00
Peer Richelsenandsean-brydon 30b433397a Removed shell from troubleshooter (#13303)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-01-20 10:29:12 -03:00
DmytroHryshyn ff1aefe539 chore: [app-router-migration 39] refactor ssr/ssg functions (#13336) 2024-01-19 15:58:37 -07:00
Crowdin Bot 5685d343b1 New Crowdin translations by Github Action 2024-01-19 22:40:08 +00:00
Peer RichelsenandUdit Takkar ae3f30fabf chore: fixed some of the terms language (#13334)
* fixed some of the terms language

* minor changes

* Update apps/web/pages/settings/organizations/[id]/onboard-admins.tsx

* fixed yarn

* nit

* fix: trans

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-01-19 22:37:04 +00:00
c11e76bac9 chore: [app-router-migration 30] migrate the "login" page (#13195)
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-01-19 15:14:14 -07:00
eed34559b5 feat: added upgrade page (#13287)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Utkarsh Nagar <utkarshnagar2000@gmail.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-01-19 21:37:40 +00:00
bf958c82da chore: [app-router-migration 32] migrate the "/auth/forgot-password/*" pages (#13197)
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-01-19 21:08:05 +00:00
Sasha 728a386bf0 chore: [app-router-migration 35] migrate the "sso" pages (#13200)
## What does this PR do?

- This PR migrates the `/auth/sso/*` page group to the app directory (which runs under the App Router).
- The migrated page exists under `/future/auth/sso/*` and is accessible at all times.

## Requirement/Documentation

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

- [x] Chore (refactoring code, technical debt, workflow improvements)
- [x] New feature (non-breaking change which adds functionality)

## How should this be tested?

There should be no changes between the old sso and new sso routes in terms of content and no downgrading in terms of performance.
The pages were not tested due to being unable to reproduce the flow locally.

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2024-01-19 14:00:36 -07:00
Sasha 41efb5ea04 chore: [app-router-migration 34] migrate the "/auth/logout" and "/auth/signin" pages (#13199)
## What does this PR do?

- This PR migrates the `/auth/logout` and `/auth/signin` pages to the app directory (which runs under the App Router).
- Using `/future/auth/logout` is available at all times and is supposed to log the user out as `/auth/logout` page does.
- However `signin` page (or fragment) was used before should behave the same using the `/future/auth/signin` route.

## Requirement/Documentation

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

- [x] Chore (refactoring code, technical debt, workflow improvements)
- [x] New feature (non-breaking change which adds functionality)

## How should this be tested?

- Using `/future/auth/logout` is available at all times and is supposed to log the user out as `/auth/logout` page does.
- However `signin` page (or fragment) was used before should behave the same using the `/future/auth/signin` route. It was not tested locally.

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2024-01-19 13:40:53 -07:00
Sasha b268540d35 chore: [app-router-migration 33] migrate the "verify email" pages (#13198)
## What does this PR do?

- This PR migrates the `/auth/verify` and `/auth/verify-email` pages to the app directory (which runs under the App Router).
- The migrated pages exist under `/future/auth/verify` and `/future/auth/verify-email` and are accessible at all times.

## Requirement/Documentation

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

- [x] Chore (refactoring code, technical debt, workflow improvements)
- [x] New feature (non-breaking change which adds functionality)

## How should this be tested?

There should be no changes between the old verify and new verify routes in terms of content and no downgrading in terms of performance.
The pages were not tested due to being unable to reproduce the flow locally.

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2024-01-19 12:02:50 -07:00
DmytroHryshynandBenny Joo bdd1d3a99d chore: [app-router-migration 16]: migrate routing forms page (#13015)
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-01-19 11:57:08 -07:00
Greg PabianandBenny Joo dd7673b1f9 chore: [app-router-migration 27] migrate the "signup" page (#13061)
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2024-01-19 11:54:43 -07:00
Nils Jacobsen 29f37a7e98 fix: update inlang settings.json (#13295)
> I'm in the core Team of inlang

## Fix inlang's configuration file

We changed the `project.inlang.json` to a `settings.json` in a `project.inlang` dir. This enables the inlang project to store app information or metadata in a dir that is not relevant to you as an end user. 

#### Old
```tree
├── project.inlang.json
```

#### New
```tree
├── project.inlang
│   ├── settings.json
```
2024-01-19 11:34:52 -07:00
Anik Dhabal BabuandUdit Takkar 3983351dac fix: Allow impersonation from the /settings/admin/users page (#13281)
* add impersonation in the dropdown menu

* add logo

* fix another minor issue

* Update UsersTable.tsx

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-01-19 17:07:38 +00:00
Peer RichelsenandKeith Williams 01322431fc chore: flappy bird ui fixes (#13329)
* Update BookEventForm.tsx

* Apply suggestions from code review

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-19 14:32:08 +00:00
Maaz Ahmed KhanandKeith Williams dcd06b0663 fix: Troubleshooter - Vertical scrolling not working on left panel or calendar (Re-opened) (#13243)
* Fix scrolling on calendar

* Fix scrolling on TroubleshooterSidebar

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-19 11:02:56 -03:00
James GarbuttandKeith Williams 26e50d24b0 test(core): add tests for events utils (#13265)
This adds tests for the following utils found in `core`:

- event utils
- getAggregateWorkingHours
- getCalendarsEvents

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-19 13:22:28 +00:00
DmytroHryshyn 74803dc122 fix: fix-e2e-tests (#13330) 2024-01-19 10:02:34 -03:00
Carina WollendorferandCarinaWolli de65b85862 show correct dates (#13309)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-19 12:50:38 +00:00
Alex van AndelandOmar López 0ee56be1d8 chore: Cache-Control new style avatar URLs (#13285)
Co-authored-by: Omar López <zomars@me.com>
2024-01-19 11:57:02 +00:00
aman sagarandUdit Takkar de6f7dcfc0 fix: destructuring error (#13312)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-01-19 10:28:35 +00:00
956f2c7a2b feat: show pay & book label to the meeting confirmation button when payment is enabled. (#12960)
* feat: Meeting confirmation button when payment is enabled says `pay & book`

* Update apps/web/public/static/locales/en/common.json

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-19 10:20:17 +00:00
14de0a1d62 test: Check the limits tab and your functionalities (teste2e-limits) (#12307)
* Check the limits tab and your functionalities

* Change way to grab element

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
2024-01-19 10:18:10 +00:00
sean-brydon 51cca7481f fix: date range sizing - ooo mobile mode (#13289) 2024-01-19 09:56:43 +00:00
Carina WollendorferandCarinaWolli 3fe0280dee fix event title in calendar event (#13316)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-01-19 09:28:47 +00:00
Syed Ali Shahbaz 876fc188d4 Make usernameList optional and remove return obj discrepancy (#13323) 2024-01-19 09:28:03 +00:00
d1810d9f14 refactor: Canceling Seats (#13136)
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2024-01-19 04:01:06 +00:00
a25cd43ddf fix: start and end date for ooo entry (#13284)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-01-19 04:00:21 +00:00
833a91375e feat: integrate avatarapi to pre-fill avatar (#13185)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-01-19 03:57:07 +00:00
8124627cc5 feat: preview of installed app name on the schedule screen (#13221)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2024-01-19 03:56:56 +00:00
Omar López 749747ad7e fix: skip e2e tests that are memory crashing (#13319) 2024-01-18 20:33:15 -07:00
Peer Richelsen 967f70a5ec fix: corrected signup condition to show social proof (#13317) 2024-01-18 15:54:02 -07:00
zomars 51c9b94fb5 chore: Syncs nextjs 2024-01-18 14:20:29 -07:00
Benny Joo 1976dd15c3 chore: [app-router-migration 37] Fix Build Errors in /bookings and /booking (#13313) 2024-01-18 13:46:42 -07:00
Anik Dhabal Babu 03671e2a58 fixes issue #13304 (#13306) 2024-01-18 16:05:43 +00:00
Nishith Munda 5e5f223908 fix: mobile-navbar (#13273) 2024-01-18 14:58:04 +00:00
zomars 9076a7eb42 fix: future build and type fixes 2024-01-17 12:01:32 -07:00
Benny Joo a9e50d76ca chore: [app-router-migration 29] Migrate /team/* page group (#13102) 2024-01-17 10:28:32 -07:00
bd4722554d chore: [app-router-migration 13.5] Add ability to reuse legacy getServerSideProps and getStaticProps on the /future pages (#13184)
Co-authored-by: Greg Pabian <35925521+grzpab@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2024-01-17 10:27:15 -07:00
zomars 105d373b8e chore: Moves docker login to e2e jobs 2024-01-17 10:25:57 -07:00
511 changed files with 10759 additions and 6273 deletions
+5
View File
@@ -138,6 +138,11 @@ FORMBRICKS_HOST_URL=https://app.formbricks.com
FORMBRICKS_ENVIRONMENT_ID=
FORMBRICKS_FEEDBACK_SURVEY_ID=
# AvatarAPI
# Used to pre-fill avatar during signup
AVATARAPI_USERNAME=
AVATARAPI_PASSWORD=
# Twilio
# Used to send SMS reminders in workflows
TWILIO_SID=
+4
View File
@@ -28,6 +28,10 @@ jobs:
shard: [1, 2]
steps:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
+4
View File
@@ -23,6 +23,10 @@ jobs:
shard: [1, 2]
steps:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
+4
View File
@@ -28,6 +28,10 @@ jobs:
shard: [1, 2]
steps:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
+4
View File
@@ -26,6 +26,10 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
-8
View File
@@ -12,14 +12,6 @@ concurrency:
cancel-in-progress: true
jobs:
login:
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
changes:
name: Detect changes
runs-on: buildjet-4vcpu-ubuntu-2204
+7 -1
View File
@@ -13,6 +13,12 @@
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/types/window.d.ts"
],
"exclude": ["node_modules"]
}
+1
View File
@@ -30,6 +30,7 @@ export const membershipEditBodySchema = Membership.omit({
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
teamId: true,
userId: true,
id: true,
})
.partial({
accepted: true,
+1 -1
View File
@@ -33,7 +33,7 @@
"@sentry/nextjs": "^7.73.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "^13.4.6",
"next": "^13.5.4",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.17.0",
"next-swagger-doc": "^0.3.6",
+98
View File
@@ -0,0 +1,98 @@
import prismock from "../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test } from "vitest";
import dayjs from "@calcom/dayjs";
import handler from "./_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
function buildMockData() {
prismock.user.create({
data: {
id: 1,
username: "test",
name: "Test User",
email: "test@example.com",
},
});
prismock.eventType.create({
data: {
id: 1,
slug: "test",
length: 30,
title: "Test Event Type",
userId: 1,
},
});
}
describe("GET /api/slots", () => {
describe("Errors", () => {
test("Missing required data", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
});
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
}
`);
});
});
describe("Success", () => {
describe("Regular event-type", () => {
test("Returns and event type available slots", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
eventTypeId: 1,
startTime: dayjs().format(),
endTime: dayjs().add(1, "day").format(),
usernameList: "test",
},
prisma: prismock,
});
buildMockData();
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"slots": {},
}
`);
});
test("Returns and event type available slots with passed timeZone", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
eventTypeId: 1,
startTime: dayjs().format(),
endTime: dayjs().add(1, "day").format(),
usernameList: "test",
timeZone: "UTC",
},
prisma: prismock,
});
buildMockData();
await handler(req, res);
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"slots": {},
}
`);
});
});
});
});
+3 -3
View File
@@ -22,7 +22,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { usernameList, ...rest } = req.query;
let slugs = usernameList;
if (!Array.isArray(usernameList)) {
slugs = usernameList ? [usernameList] : [];
slugs = usernameList ? [usernameList] : undefined;
}
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
@@ -38,9 +38,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
{}
)
: availableSlots;
: availableSlots.slots;
return slotsInProvidedTimeZone;
return { slots: slotsInProvidedTimeZone };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (cause) {
if (cause instanceof TRPCError) {
+1 -1
View File
@@ -92,7 +92,7 @@ export async function patchHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
if (args.data.eventTriggers) {
if (eventTriggers) {
const eventTriggersSet = new Set(eventTriggers);
args.data.eventTriggers = Array.from(eventTriggersSet);
}
+1 -1
View File
@@ -92,7 +92,7 @@ async function postHandler(req: NextApiRequest) {
args.data.userId = bodyUserId;
}
if (args.data.eventTriggers) {
if (eventTriggers) {
const eventTriggersSet = new Set(eventTriggers);
args.data.eventTriggers = Array.from(eventTriggersSet);
}
+1 -1
View File
@@ -24,7 +24,7 @@
"@storybook/blocks": "^7.6.3",
"@storybook/nextjs": "^7.6.3",
"@storybook/preview-api": "^7.6.3",
"next": "^13.4.6",
"next": "^13.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook-addon-rtl-direction": "^0.0.19"
+1 -1
View File
@@ -14,7 +14,7 @@
"dependencies": {
"highlight.js": "^11.6.0",
"isarray": "2.0.5",
"next": "^13.4.6",
"next": "^13.5.4",
"openapi-snippet": "^0.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+24
View File
@@ -0,0 +1,24 @@
import type { GetStaticProps, GetStaticPropsContext } from "next";
import { notFound, redirect } from "next/navigation";
export const withAppDirSsg =
<T extends Record<string, any>>(getStaticProps: GetStaticProps<T>) =>
async (context: GetStaticPropsContext) => {
const ssgResponse = await getStaticProps(context);
if ("redirect" in ssgResponse) {
redirect(ssgResponse.redirect.destination);
}
if ("notFound" in ssgResponse) {
notFound();
}
const props = await Promise.resolve(ssgResponse.props);
return {
...ssgResponse.props,
// includes dehydratedState required for future page trpcPropvider
...("trpcState" in props && { dehydratedState: props.trpcState }),
};
};
@@ -1,9 +1,9 @@
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
import { notFound, redirect } from "next/navigation";
export const withAppDir =
export const withAppDirSsr =
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
async (context: GetServerSidePropsContext): Promise<T> => {
async (context: GetServerSidePropsContext) => {
const ssrResponse = await getServerSideProps(context);
if ("redirect" in ssrResponse) {
@@ -13,9 +13,11 @@ export const withAppDir =
notFound();
}
const props = await Promise.resolve(ssrResponse.props);
return {
...ssrResponse.props,
...props,
// includes dehydratedState required for future page trpcPropvider
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
...("trpcState" in props && { dehydratedState: props.trpcState }),
};
};
+9 -4
View File
@@ -1,11 +1,16 @@
"use client";
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
import { useMemo } from "react";
import type { DataTransformer } from "@trpc/server";
type DataTransformer = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serialize: (data: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deserialize: (data: any) => any;
};
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
export function createHydrateClient(opts: { transformer: DataTransformer }) {
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
const { state, children } = props;
@@ -16,6 +21,6 @@ export function createHydrateClient(opts: { transformer?: DataTransformer }) {
return state;
}, [state]);
return <Hydrate state={transformedState}>{children}</Hydrate>;
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
};
}
+1 -4
View File
@@ -4,10 +4,7 @@ import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
import { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
export type Endpoint = (typeof ENDPOINTS)[number];
@@ -0,0 +1,197 @@
import LegacyPage, { getLayout } from "@pages/apps/[slug]/[...pages]";
import type { PageProps, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import type { GetServerSidePropsContext } from "next";
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
import { cookies, headers } from "next/headers";
import { notFound, redirect } from "next/navigation";
import z from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import RoutingFormsRoutingConfig, {
serverSidePropsConfig,
} 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 { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import type { AppProps } from "@lib/app-providers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import PageWrapper from "@components/PageWrapperAppDir";
import { ssrInit } from "@server/lib/ssr";
type AppPageType = {
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: AppPageType["default"];
getServerSideProps: AppPageType["getServerSideProps"];
};
const AppsRouting = {
"routing-forms": RoutingFormsRoutingConfig,
typeform: TypeformRoutingConfig,
};
const paramsSchema = z.object({
slug: z.string(),
pages: z.array(z.string()),
});
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const mainPage = p.data.pages[0];
if (mainPage === "forms") {
return await _generateMetadata(
() => `Forms | ${APP_NAME}`,
() => ""
);
}
const legacyContext = buildLegacyCtx(
headers(),
cookies(),
params,
searchParams
) as unknown as GetServerSidePropsContext;
const { form } = await getPageProps(legacyContext);
return await _generateMetadata(
() => `${form.name} | ${APP_NAME}`,
() => form.description
);
};
function getRoute(appName: string, pages: string[]) {
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record<string, AppPageType>;
if (!routingConfig) {
notFound();
}
const mainPage = pages[0];
const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
const getServerSidePropsHandler = serverSidePropsConfig[mainPage];
if (!appPage) {
notFound();
}
return {
notFound: false,
Component: appPage.default,
...appPage,
getServerSideProps: getServerSidePropsHandler,
} as Found;
}
const getPageProps = async ({ params, query, req }: GetServerSidePropsContext) => {
const p = paramsSchema.safeParse(params);
if (!p.success) {
return notFound();
}
const { slug: appName, pages } = p.data;
const route = getRoute(appName, pages);
if (route.notFound) {
return route;
}
if (route.getServerSideProps) {
// TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
params!.appPages = pages.slice(1);
const ctx = { req, params, query };
const session = await getServerSession({ req });
const user = session?.user;
const app = await getAppWithMetadata({ slug: appName });
if (!app) {
notFound();
}
const result = await route.getServerSideProps(
{
...ctx,
params: {
...ctx.params,
appPages: pages.slice(1),
},
} as GetServerSidePropsContext<{
slug: string;
pages: string[];
appPages: string[];
}>,
prisma,
user,
ssrInit
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (result.notFound) {
notFound();
}
if (result.redirect) {
redirect(result.redirect.destination);
}
return {
appName,
appUrl: app.simplePath || `/apps/${appName}`,
...result.props,
};
} else {
return {
appName,
};
}
};
export default async function Page({ params, searchParams }: PageProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const legacyContext = buildLegacyCtx(
h,
cookies(),
params,
searchParams
) as unknown as GetServerSidePropsContext;
const props = await getPageProps(legacyContext);
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
<LegacyPage {...props} />
</PageWrapper>
);
}
@@ -1,3 +0,0 @@
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null })<"L">;
+23 -98
View File
@@ -1,40 +1,33 @@
import AppPage from "@pages/apps/[slug]/index";
import Page from "@pages/apps/[slug]/index";
import { Prisma } from "@prisma/client";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import fs from "fs";
import matter from "gray-matter";
import { notFound } from "next/navigation";
import path from "path";
import { z } from "zod";
import { WithLayout } from "app/layoutHOC";
import type { InferGetStaticPropsType } from "next";
import { cookies, headers } from "next/headers";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
const sourceSchema = z.object({
content: z.string(),
data: z.object({
description: z.string().optional(),
items: z
.array(
z.union([
z.string(),
z.object({
iframe: z.object({ src: z.string() }),
}),
])
)
.optional(),
}),
});
import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
const { data } = await getPageProps({ params });
type Y = InferGetStaticPropsType<typeof getStaticProps>;
const getData = withAppDirSsg<Y>(getStaticProps);
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
const res = await getData(legacyContext);
return await _generateMetadata(
() => `${data.name} | ${APP_NAME}`,
() => data.description
() => `${res?.data.name} | ${APP_NAME}`,
() => res?.data.description ?? ""
);
};
@@ -53,74 +46,6 @@ export const generateStaticParams = async () => {
return [];
};
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
if (typeof params?.slug !== "string") {
notFound();
}
const appMeta = await getAppWithMetadata({
slug: params?.slug,
});
const appFromDb = await prisma.app.findUnique({
where: { slug: params.slug.toLowerCase() },
});
const isAppAvailableInFileSystem = appMeta;
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
if (!IS_PRODUCTION && isAppDisabled) {
return {
isAppDisabled: true as const,
data: {
...appMeta,
},
};
}
if (!appFromDb || !appMeta || isAppDisabled) {
notFound();
}
const isTemplate = appMeta.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
const postFilePath = path.join(README_PATH);
let source = "";
try {
source = fs.readFileSync(postFilePath).toString();
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
} catch (error) {
/* If the app doesn't have a README we fallback to the package description */
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
source = appMeta.description;
}
const result = matter(source);
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
if (data.items) {
data.items = data.items.map((item) => {
if (typeof item === "string") {
return getAppAssetFullPath(item, {
dirName: appMeta.dirName,
isTemplate: appMeta.isTemplate,
});
}
return item;
});
}
return {
isAppDisabled: false as const,
source: { content, data },
data: appMeta,
};
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
const pageProps = await getPageProps({ params });
return <AppPage {...pageProps} />;
}
export default WithLayout({ getLayout: null, Page, getData });
export const dynamic = "force-static";
+7 -22
View File
@@ -1,8 +1,8 @@
import SetupPage from "@pages/apps/[slug]/setup";
import Page from "@pages/apps/[slug]/setup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { notFound, redirect } from "next/navigation";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
import { APP_NAME } from "@calcom/lib/constants";
@@ -14,23 +14,8 @@ export const generateMetadata = async ({ params }: { params: Record<string, stri
);
};
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
const req = { headers: headers(), cookies: cookies() };
type T = InferGetServerSidePropsType<typeof getServerSideProps>;
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
const getData = withAppDirSsr<T>(getServerSideProps);
if (!result || "notFound" in result) {
notFound();
}
if ("redirect" in result) {
redirect(result.redirect.destination);
}
return result.props;
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
const pageProps = await getPageProps({ params });
return <SetupPage {...pageProps} />;
}
export default WithLayout({ getLayout: null, Page, getData });
@@ -1,15 +1,15 @@
import CategoryPage from "@pages/apps/categories/[category]";
import CategoryPage, { type PageProps } from "@pages/apps/categories/[category]";
import { Prisma } from "@prisma/client";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import z from "zod";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
export const generateMetadata = async () => {
return await _generateMetadata(
() => `${APP_NAME} | ${APP_NAME}`,
@@ -34,38 +34,7 @@ export const generateStaticParams = async () => {
return paths.map((category) => ({ category }));
};
const querySchema = z.object({
category: z.nativeEnum(AppCategories),
});
const getData = withAppDirSsg<PageProps>(getStaticProps);
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
const p = querySchema.safeParse(params);
if (!p.success) {
return notFound();
}
const appQuery = await prisma.app.findMany({
where: {
categories: {
has: p.data.category,
},
},
select: {
slug: true,
},
});
const dbAppsSlugs = appQuery.map((category) => category.slug);
const appStore = await getAppRegistry();
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
return {
apps,
};
};
// @ts-expect-error getData arg
export default WithLayout({ getData: getPageProps, Page: CategoryPage })<"P">;
export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">;
export const dynamic = "force-static";
+4 -31
View File
@@ -1,13 +1,11 @@
import LegacyPage from "@pages/apps/categories/index";
import Page from "@pages/apps/categories/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import { ssrInit } from "@server/lib/ssr";
import { getServerSideProps } from "@lib/apps/categories/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
@@ -16,29 +14,4 @@ export const generateMetadata = async () => {
);
};
const getData = async (ctx: GetServerSidePropsContext) => {
const ssr = await ssrInit(ctx);
const session = await getServerSession({ req: ctx.req });
let appStore;
if (session?.user?.id) {
appStore = await getAppRegistryWithCredentials(session.user.id);
} else {
appStore = await getAppRegistry();
}
const categories = appStore.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
return {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
dehydratedState: ssr.dehydrate(),
};
};
export default WithLayout({ getData, Page: LegacyPage, getLayout: null })<"P">;
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), Page, getLayout: null })<"P">;
@@ -1,3 +0,0 @@
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null })<"L">;
@@ -1,14 +1,11 @@
import LegacyPage from "@pages/apps/installed/[category]";
import Page from "@pages/apps/installed/[category]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation";
import { z } from "zod";
import { WithLayout } from "app/layoutHOC";
import { APP_NAME } from "@calcom/lib/constants";
import { AppCategories } from "@calcom/prisma/enums";
const querySchema = z.object({
category: z.nativeEnum(AppCategories),
});
import { getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
@@ -17,20 +14,6 @@ export const generateMetadata = async () => {
);
};
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
const p = querySchema.safeParse(params);
const getData = withAppDirSsr(getServerSideProps);
if (!p.success) {
return notFound();
}
return {
category: p.data.category,
};
};
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
await getPageProps({ params });
return <LegacyPage />;
}
export default WithLayout({ getLayout: null, getData, Page });
+3 -48
View File
@@ -1,17 +1,12 @@
import AppsPage from "@pages/apps";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { APP_NAME } from "@calcom/lib/constants";
import type { AppCategories } from "@calcom/prisma/enums";
import { ssrInit } from "@server/lib/ssr";
import { getServerSideProps } from "@lib/apps/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
@@ -20,44 +15,4 @@ export const generateMetadata = async () => {
);
};
const getData = async (ctx: GetServerSidePropsContext) => {
const ssr = await ssrInit(ctx);
const session = await getServerSession({ req: ctx.req });
let appStore, userAdminTeams: UserAdminTeams;
if (session?.user?.id) {
userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
} else {
appStore = await getAppRegistry();
userAdminTeams = [];
}
const categoryQuery = appStore.map(({ categories }) => ({
categories: categories || [],
}));
const categories = categoryQuery.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
return {
categories: Object.entries(categories)
.map(([name, count]): { name: AppCategories; count: number } => ({
name: name as AppCategories,
count,
}))
.sort(function (a, b) {
return b.count - a.count;
}),
appStore,
userAdminTeams,
dehydratedState: ssr.dehydrate(),
};
};
export default WithLayout({ getLayout, getData, Page: AppsPage });
export default WithLayout({ getLayout, getData: withAppDirSsr(getServerSideProps), Page: AppsPage });
@@ -0,0 +1,19 @@
import SetNewUserPassword from "@pages/auth/forgot-password/[id]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("reset_password"),
(t) => t("change_your_password")
);
};
export default WithLayout({
getLayout: null,
Page: SetNewUserPassword,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
@@ -0,0 +1,19 @@
import ForgotPassword from "@pages/auth/forgot-password";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("reset_password"),
(t) => t("change_your_password")
);
};
export default WithLayout({
getLayout: null,
Page: ForgotPassword,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
+22
View File
@@ -0,0 +1,22 @@
import Login from "@pages/auth/login";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { APP_NAME } from "@calcom/lib/constants";
import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => `${t("login")} | ${APP_NAME}`,
(t) => t("login")
);
};
export default WithLayout({
getLayout: null,
Page: Login,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
})<"P">;
+7
View File
@@ -0,0 +1,7 @@
import Logout from "@pages/auth/logout";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
export default WithLayout({ getLayout: null, Page: Logout, getData: withAppDirSsr(getServerSideProps) })<"P">;
+12
View File
@@ -0,0 +1,12 @@
import Setup from "@pages/auth/setup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: Setup,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
})<"P">;
+13
View File
@@ -0,0 +1,13 @@
import signin from "@pages/auth/signin";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: signin,
// @ts-expect-error TODO: fix this
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
})<"P">;
@@ -0,0 +1,12 @@
import Provider from "@pages/auth/sso/[provider]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import type { InferGetServerSidePropsType } from "next";
import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: Provider,
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
})<"P">;
@@ -0,0 +1,11 @@
import DirectSSOLogin from "@pages/auth/sso/direct";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: DirectSSOLogin,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
@@ -0,0 +1,15 @@
import VerifyEmailPage from "@pages/auth/verify-email";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () => {
return await _generateMetadata(
(t) => t("check_your_email"),
(t) => t("check_your_email")
);
};
export default WithLayout({
getLayout: null,
Page: VerifyEmailPage,
})<"P">;
+11
View File
@@ -0,0 +1,11 @@
import VerifyPage from "@pages/auth/verify";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps";
export default WithLayout({
getLayout: null,
Page: VerifyPage,
getData: withAppDirSsr(getServerSideProps),
})<"P">;
@@ -1,10 +1,11 @@
import OldPage from "@pages/booking/[uid]";
import { type PageProps, getServerSideProps } from "@pages/booking/[uid]";
import OldPage from "@pages/booking/[uid]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { getData } from "../page";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
// @ts-expect-error Type '(context: GetServerSidePropsContext) => Promise<any>' is not assignable to type '(arg: {
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
+26 -194
View File
@@ -1,204 +1,36 @@
import OldPage from "@pages/booking/[uid]";
import OldPage, { type PageProps } from "@pages/booking/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { z } from "zod";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { BookingStatus } from "@calcom/prisma/enums";
import { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } from "@lib/booking";
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
const stringToBoolean = z
.string()
.optional()
.transform((val) => val === "true");
const querySchema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(),
seatReferenceUid: z.string().optional(),
});
export const generateMetadata = async () =>
await _generateMetadata(
() => "",
() => ""
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const { bookingInfo, eventType, recurringBookings } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
export const getData = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getServerSession(context);
let tz: string | null = null;
let userTimeFormat: number | null = null;
let requiresLoginToUpdate = false;
if (session) {
const user = await ssr.viewer.me.fetch();
tz = user.timeZone;
userTimeFormat = user.timeFormat;
}
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) {
notFound();
}
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
uid: maybeUid,
},
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
metadata: true,
cancellationReason: true,
responses: true,
rejectionReason: true,
user: {
select: {
id: true,
name: true,
email: true,
username: true,
timeZone: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
eventTypeId: true,
eventType: {
select: {
eventName: true,
slug: true,
timeZone: true,
},
},
seatsReferences: {
select: {
referenceUid: true,
},
},
},
});
if (!bookingInfoRaw) {
notFound();
}
const eventTypeRaw = !bookingInfoRaw.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
if (!eventTypeRaw) {
notFound();
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
requiresLoginToUpdate = true;
}
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
? eventTypeRaw.hosts.map((host) => host.user)
: eventTypeRaw.users;
if (!eventTypeRaw.users.length) {
if (!eventTypeRaw.owner) {
notFound();
}
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
}
const eventType = {
...eventTypeRaw,
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
}
const payment = await prisma.payment.findFirst({
where: {
bookingId: bookingInfo.id,
},
select: {
success: true,
refunded: true,
currency: true,
amount: true,
paymentOption: true,
},
});
return {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
dehydratedState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
paymentStatus: payment,
...(tz && { tz }),
userTimeFormat,
requiresLoginToUpdate,
};
return await _generateMetadata(
(t) =>
t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`),
(t) =>
t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`)
);
};
// @ts-expect-error Argument of type '{ req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page: OldPage });
@@ -1,45 +0,0 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import { ssgInit } from "@server/lib/ssg";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const querySchema = z.object({
status: z.enum(validStatuses),
});
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${APP_NAME} | ${t("bookings")}`,
() => ""
);
export const generateStaticParams = async () => {
return validStatuses.map((status) => ({ status }));
};
const getData = async (ctx: GetServerSidePropsContext) => {
const parsedParams = querySchema.safeParse(ctx.params);
if (!parsedParams.success) {
notFound();
}
const ssg = await ssgInit(ctx);
return {
status: parsedParams.data.status,
dehydratedState: ssg.dehydrate(),
};
};
export default WithLayout({ getLayout, getData })<"L">;
export const dynamic = "force-static";
+29 -1
View File
@@ -1 +1,29 @@
export { default } from "@pages/bookings/[status]";
import Page from "@pages/bookings/[status]";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { InferGetStaticPropsType } from "next";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import { getStaticProps } from "@lib/bookings/[status]/getStaticProps";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
type Y = InferGetStaticPropsType<typeof getStaticProps>;
const getData = withAppDirSsg<Y>(getStaticProps);
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${APP_NAME} | ${t("bookings")}`,
() => ""
);
export const generateStaticParams = async () => {
return validStatuses.map((status) => ({ status }));
};
export default WithLayout({ getLayout, getData, Page })<"P">;
export const dynamic = "force-static";
+13 -109
View File
@@ -1,27 +1,21 @@
import LegacyPage, { type PageProps } from "@pages/d/[link]/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import LegacyPage from "@pages/d/[link]/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { Params, SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
const pageProps = await getPageProps(
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
);
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Params;
searchParams: SearchParams;
}) => {
const pageProps = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
const { entity, booking, user, slug, isTeamEvent } = pageProps;
const rescheduleUid = booking?.uid;
@@ -38,95 +32,5 @@ export const generateMetadata = async ({ params }: { params: Record<string, stri
);
};
async function getPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession({ req: context.req });
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid, duration: queryDuration } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: {
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
},
});
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return notFound();
}
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return notFound();
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
}
const isTeamEvent = !!hashedLink.eventType?.team?.id;
const ssr = await ssrInit(context);
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent, org });
if (!eventData) {
return notFound();
}
return {
entity: eventData.entity,
duration: getMultipleDurationValue(eventData.metadata?.multipleDuration, queryDuration, eventData.length),
booking,
away: user?.away,
user: username,
slug,
dehydratedState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
hashedLink: link,
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// @ts-expect-error arg
const getData = withAppDir<PageProps>(getPageProps);
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
+4 -1
View File
@@ -1,4 +1,7 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import EnterprisePage from "@components/EnterprisePage";
@@ -8,4 +11,4 @@ export const generateMetadata = async () =>
(t) => t("create_your_org_description")
);
export default EnterprisePage;
export default WithLayout({ getLayout, Page: EnterprisePage })<"P">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
+4 -1
View File
@@ -1,5 +1,8 @@
import EventTypes from "@pages/event-types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -7,4 +10,4 @@ export const generateMetadata = async () =>
(t) => t("event_types_page_subtitle")
);
export default EventTypes;
export default WithLayout({ getLayout, Page: EventTypes })<"P">;
@@ -1,57 +1,7 @@
import LegacyPage from "@pages/getting-started/[[...step]]";
import Page from "@pages/getting-started/[[...step]]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
import { ssrInit } from "@server/lib/ssr";
const getData = async (ctx: GetServerSidePropsContext) => {
const session = await getServerSession({ req: ctx.req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
const ssr = await ssrInit(ctx);
await ssr.viewer.me.prefetch();
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
completedOnboarding: true,
teams: {
select: {
accepted: true,
team: {
select: {
id: true,
name: true,
logo: true,
},
},
},
},
},
});
if (!user) {
throw new Error("User from session not found");
}
if (user.completedOnboarding) {
redirect("/event-types");
}
return {
dehydratedState: ssr.dehydrate(),
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
requiresLicense: false,
themeBasis: null,
};
};
export default WithLayout({ getLayout: null, getData, Page: LegacyPage });
export default WithLayout({ getLayout: null, getData: withAppDirSsr(getServerSideProps), Page });
+5 -13
View File
@@ -1,10 +1,11 @@
import LegacyPage from "@pages/insights/index";
import LegacyPage, { getServerSideProps } from "@pages/insights/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -12,15 +13,6 @@ export const generateMetadata = async () =>
(t) => t("insights_subtitle")
);
async function getData() {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
if (flags.insights === false) {
return notFound();
}
return {};
}
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
export default WithLayout({ getLayout, getData, Page: LegacyPage });
@@ -1,13 +1,12 @@
import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
import { withAppDir } from "app/AppDirSSRHOC";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
const getData = withAppDir<PageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
@@ -1,9 +1,9 @@
import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
import { withAppDir } from "app/AppDirSSRHOC";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
const getData = withAppDir<PageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
@@ -1,10 +1,11 @@
import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import Page from "@pages/org/[orgSlug]/[user]/embed";
import { withAppDir } from "app/AppDirSSRHOC";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDir<PageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
@@ -1,7 +1,7 @@
import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
import { withAppDir } from "app/AppDirSSRHOC";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDir<PageProps>(getServerSideProps);
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
+3 -152
View File
@@ -1,18 +1,10 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect, notFound } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
import { getServerSideProps, type PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
import { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -21,147 +13,6 @@ export const generateMetadata = async () =>
() => ""
);
const querySchema = z.object({
uid: z.string(),
});
async function getData(context: GetServerSidePropsContext) {
const session = await getServerSession({ req: context.req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const { uid } = querySchema.parse(context.params);
const rawPayment = await prisma.payment.findFirst({
where: {
uid,
},
select: {
data: true,
success: true,
uid: true,
refunded: true,
bookingId: true,
appId: true,
amount: true,
currency: true,
paymentOption: true,
booking: {
select: {
id: true,
uid: true,
description: true,
title: true,
startTime: true,
endTime: true,
attendees: {
select: {
email: true,
name: true,
},
},
eventTypeId: true,
location: true,
status: true,
rejectionReason: true,
cancellationReason: true,
eventType: {
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
requiresConfirmation: true,
userId: true,
metadata: true,
users: {
select: {
name: true,
username: true,
hideBranding: true,
theme: true,
},
},
team: {
select: {
name: true,
hideBranding: true,
},
},
price: true,
currency: true,
successRedirectUrl: true,
},
},
},
},
},
});
if (!rawPayment) {
return notFound();
}
const { data, booking: _booking, ...restPayment } = rawPayment;
const payment = {
...restPayment,
data: data as Record<string, unknown>,
};
if (!_booking) {
return notFound();
}
const { startTime, endTime, eventType, ...restBooking } = _booking;
const booking = {
...restBooking,
startTime: startTime.toString(),
endTime: endTime.toString(),
};
if (!eventType) {
return notFound();
}
if (eventType.users.length === 0 && !!!eventType.team) {
return notFound();
}
const [user] = eventType?.users.length
? eventType.users
: [{ name: null, theme: null, hideBranding: null, username: null }];
const profile = {
name: eventType.team?.name || user?.name || null,
theme: (!eventType.team?.name && user?.theme) || null,
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
};
if (
([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes(
booking.status as BookingStatus
)
) {
return redirect(`/booking/${booking.uid}`);
}
return {
user,
eventType: {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
},
booking,
dehydratedState: ssr.dehydrate(),
payment,
clientSecret: getClientSecretFromPayment(payment),
profile,
};
}
const getData = withAppDirSsr<PaymentPageProps>(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page: PaymentPage });
@@ -1,5 +1,6 @@
import { getServerSideProps } from "@pages/reschedule/[uid]";
import { withAppDir } from "app/AppDirSSRHOC";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { SearchParams } from "app/_types";
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
import { cookies, headers } from "next/headers";
@@ -8,12 +9,12 @@ import withEmbedSsr from "@lib/withEmbedSsr";
type PageProps = Readonly<{
params: Params;
searchParams: SearchParams;
}>;
const Page = async ({ params }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
await withAppDir(withEmbedSsr(getServerSideProps))(legacyCtx);
const Page = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx);
return null;
};
@@ -1,5 +1,6 @@
import OldPage, { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]";
import { withAppDir } from "app/AppDirSSRHOC";
import { getServerSideProps } from "@pages/reschedule/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { SearchParams } from "app/_types";
import { _generateMetadata } from "app/_utils";
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
import { headers, cookies } from "next/headers";
@@ -14,17 +15,17 @@ export const generateMetadata = async () =>
type PageProps = Readonly<{
params: Params;
searchParams: SearchParams;
}>;
const getData = withAppDir(_getServerSideProps);
const getData = withAppDirSsr(getServerSideProps);
const Page = async ({ params }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params);
const Page = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }'
await getData(legacyCtx);
return <OldPage />;
return null;
};
export default Page;
@@ -0,0 +1,12 @@
import { redirect } from "next/navigation";
const getPageProps = () => {
return redirect(`/apps/routing-forms/forms`);
};
const Page = () => {
getPageProps();
return null;
};
export default Page;
@@ -0,0 +1,36 @@
import { type SearchParams } from "app/_types";
import { type GetServerSidePropsContext } from "next";
import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import z from "zod";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
const paramsSchema = z
.object({
pages: z.array(z.string()),
})
.catch({
pages: [],
});
const getPageProps = async (context: GetServerSidePropsContext) => {
const { pages } = paramsSchema.parse(context.params);
return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
};
type PageProps = Readonly<{
params: Params;
searchParams: SearchParams;
}>;
const Page = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
await getPageProps(legacyCtx as unknown as GetServerSidePropsContext);
return null;
};
export default Page;
@@ -1,36 +1,32 @@
import { getServerCaller } from "app/_trpc/serverClient";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { WithLayout } from "app/layoutHOC";
import { z } from "zod";
import Page from "@calcom/features/ee/users/pages/users-edit-view";
import prisma from "@calcom/prisma";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
const userIdSchema = z.object({ id: z.coerce.number() });
export const generateMetadata = async ({ params }: { params: Params }) => {
const input = userIdSchema.safeParse(params);
let title = "";
if (!input.success) {
title = "Editing user";
} else {
const req = {
headers: headers(),
cookies: cookies(),
};
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest'
const data = await getServerCaller({ req, prisma }).viewer.users.get({ userId: input.data.id });
const { user } = data;
title = `Editing user: ${user.username}`;
return await _generateMetadata(
() => "",
() => "Here you can edit a current user."
);
}
const userId = input.data.id;
const { trpc } = await import("@calcom/trpc");
const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId });
const { user } = data;
const title = `Editing user: ${user.username}`;
return await _generateMetadata(
() => title,
() => "Here you can edit a current user."
);
};
export default Page;
export default WithLayout({ getLayout, Page })<"P">;
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import Page from "@calcom/features/ee/users/pages/users-add-view";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,4 @@ export const generateMetadata = async () =>
() => "Here you can add a new user."
);
export default Page;
export default WithLayout({ getLayout, Page })<"P">;
@@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout })<"L">;
@@ -1,6 +1,8 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import Page from "@calcom/features/ee/users/pages/users-listing-view";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -8,4 +10,4 @@ export const generateMetadata = async () =>
() => "A list of all the users in your account including their name, title, email and role."
);
export default Page;
export default WithLayout({ getLayout, Page })<"P">;
@@ -1,6 +1,6 @@
import LegacyPage, {
buildWrappedOnboardTeamMembersPage,
} from "@pages/settings/organizations/[id]/onboard-admins";
} from "@pages/settings/organizations/[id]/onboard-members";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { headers } from "next/headers";
@@ -1,10 +1,9 @@
import LegacyPage, { WrappedCreateNewOrganizationPage } from "@pages/settings/organizations/new/index";
import LegacyPage, { getServerSideProps, LayoutWrapperAppDir } from "@pages/settings/organizations/new/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { type inferSSRProps } from "@lib/types/inferSSRProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -12,23 +11,8 @@ export const generateMetadata = async () =>
(t) => t("organizations_description")
);
const getPageProps = async (context: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
// Check if organizations are enabled
if (flags["organizations"] !== true) {
return notFound();
}
const querySlug = context.query.slug as string;
return {
querySlug: querySlug ?? null,
};
};
export default WithLayout({
getLayout: WrappedCreateNewOrganizationPage,
getLayout: LayoutWrapperAppDir,
Page: LegacyPage,
getData: getPageProps,
getData: withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps),
});
+20
View File
@@ -0,0 +1,20 @@
import LegacyPage, { type SignupProps } from "@pages/signup";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/signup/getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sign_up"),
(t) => t("sign_up")
);
const getData = withAppDirSsr<SignupProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getLayout: null,
getData,
})<"P">;
@@ -0,0 +1,10 @@
import LegacyPage, { getServerSideProps } from "@pages/team/[slug]/[type]/embed";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDirSsr(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
// @ts-expect-error Type is missing the following properties from type: entity, duration, booking, away, and 7 more.
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
@@ -0,0 +1,41 @@
import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]/[type]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
const { entity, user, slug, booking } = props;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{ username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
{ refetchOnWindowFocus: false }
);
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: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
@@ -0,0 +1,9 @@
import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
const getData = withAppDirSsr<PageProps>(getServerSideProps);
const getEmbedData = withEmbedSsrAppDir(getData);
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+35
View File
@@ -0,0 +1,35 @@
import LegacyPage, { getServerSideProps, type PageProps } from "@pages/team/[slug]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const props = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
const teamName = props.team.name || "Nameless Team";
return await _generateMetadata(
() => teamName,
() => teamName
);
};
const getData = withAppDirSsr<PageProps>(getServerSideProps);
export default WithLayout({
Page: LegacyPage,
getData,
getLayout: null,
isBookingPage: true,
})<"P">;
+4 -25
View File
@@ -1,13 +1,11 @@
import OldPage from "@pages/teams/index";
import Page from "@pages/teams/index";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { ssrInit } from "@server/lib/ssr";
import { getServerSideProps } from "@lib/teams/getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -15,23 +13,4 @@ export const generateMetadata = async () =>
(t) => t("create_manage_teams_collaborative")
);
async function getData(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({
req: context.req,
});
if (!session) {
const token = Array.isArray(context.query.token) ? context.query.token[0] : context.query.token;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
return redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
}
return { dehydratedState: ssr.dehydrate() };
}
export default WithLayout({ getData, getLayout, Page: OldPage })<"P">;
export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout, Page })<"P">;
+5 -89
View File
@@ -1,15 +1,11 @@
import OldPage from "@pages/video/[uid]";
import Page, { type JoinCallPageProps } from "@pages/video/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MarkdownIt from "markdown-it";
import type { GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { ssrInit } from "@server/lib/ssr";
import { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -17,86 +13,6 @@ export const generateMetadata = async () =>
(t) => t("quick_video_meeting")
);
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
const getData = withAppDirSsr<JoinCallPageProps>(getServerSideProps);
async function getData(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({
where: {
uid: context.query.uid as string,
},
select: {
...bookingMinimalSelect,
uid: true,
description: true,
isRecorded: true,
user: {
select: {
id: true,
timeZone: true,
name: true,
email: true,
organization: {
select: {
calVideoLogo: true,
},
},
},
},
references: {
select: {
uid: true,
type: true,
meetingUrl: true,
meetingPassword: true,
},
where: {
type: "daily_video",
},
},
},
});
if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) {
return redirect("/video/no-meeting-found");
}
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
const now = new Date();
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
//find out if the meeting is in the past
const isPast = booking?.endTime <= exitDate;
if (isPast) {
return redirect(`/video/meeting-ended/${booking?.uid}`);
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
const session = await getServerSession({ req: context.req });
// set meetingPassword to null for guests
if (session?.user.id !== bookingObj.user?.id) {
bookingObj.references.forEach((bookRef) => {
bookRef.meetingPassword = null;
});
}
return {
meetingUrl: bookingObj.references[0].meetingUrl ?? "",
...(typeof bookingObj.references[0].meetingPassword === "string" && {
meetingPassword: bookingObj.references[0].meetingPassword,
}),
booking: {
...bookingObj,
...(bookingObj.description && { description: md.render(bookingObj.description) }),
},
dehydratedState: ssr.dehydrate(),
};
}
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;
export default WithLayout({ getData, Page, getLayout: null })<"P">;
@@ -1,10 +1,9 @@
import OldPage from "@pages/video/meeting-ended/[uid]";
import Page from "@pages/video/meeting-ended/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { getServerSideProps } from "@lib/video/meeting-ended/[uid]/getServerSideProps";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -12,41 +11,6 @@ export const generateMetadata = async () =>
() => "Meeting Unavailable"
);
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const booking = await prisma.booking.findUnique({
where: {
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
},
select: {
...bookingMinimalSelect,
uid: true,
user: {
select: {
credentials: true,
},
},
references: {
select: {
uid: true,
type: true,
meetingUrl: true,
},
},
},
});
const getData = withAppDirSsr(getServerSideProps);
if (!booking) {
return redirect("/video/no-meeting-found");
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
return {
booking: bookingObj,
};
}
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;
export default WithLayout({ getData, Page, getLayout: null })<"P">;
@@ -1,15 +1,12 @@
import OldPage from "@pages/video/meeting-not-started/[uid]";
import { type Params } from "app/_types";
import Page from "@pages/video/meeting-not-started/[uid]";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
type PageProps = Readonly<{
params: Params;
}>;
import { getServerSideProps } from "@lib/video/meeting-not-started/[uid]/getServerSideProps";
export const generateMetadata = async ({ params }: PageProps) => {
const booking = await prisma.booking.findUnique({
@@ -25,26 +22,6 @@ export const generateMetadata = async ({ params }: PageProps) => {
);
};
async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolvedUrl">) {
const booking = await prisma.booking.findUnique({
where: {
uid: typeof context?.params?.uid === "string" ? context.params.uid : "",
},
select: bookingMinimalSelect,
});
const getData = withAppDirSsr(getServerSideProps);
if (!booking) {
return redirect("/video/no-meeting-found");
}
const bookingObj = Object.assign({}, booking, {
startTime: booking.startTime.toString(),
endTime: booking.endTime.toString(),
});
return {
booking: bookingObj,
};
}
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;
export default WithLayout({ getData, Page, getLayout: null })<"P">;
@@ -1,7 +1,7 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import type { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
@@ -11,6 +11,7 @@ export const generateMetadata = async () =>
(t) => t("no_meeting_found")
);
// ssr was added by Intuita, legacy page does not have it
const getData = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
@@ -1,21 +1,21 @@
import LegacyPage, { getStaticProps } from "@pages/workflows/[workflow]";
import { withAppDirSsg } from "app/WithAppDirSsg";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { headers, cookies } from "next/headers";
import { notFound } from "next/navigation";
import { z } from "zod";
import LegacyPage from "@calcom/features/ee/workflows/pages/workflow";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
const querySchema = z.object({
workflow: z.string(),
});
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
const { workflow } = await getProps(
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
export const generateMetadata = async ({
params,
searchParams,
}: {
params: Record<string, string | string[]>;
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const { workflow } = await getData(
buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
);
return await _generateMetadata(
() => workflow ?? "Untitled",
@@ -23,19 +23,12 @@ export const generateMetadata = async ({ params }: { params: Record<string, stri
);
};
async function getProps(context: GetServerSidePropsContext) {
const safeParams = querySchema.safeParse(context.params);
console.log("Built workflow page:", safeParams);
if (!safeParams.success) {
return notFound();
}
return { workflow: safeParams.data.workflow };
}
const getData = withAppDirSsg(getStaticProps);
export const generateStaticParams = () => [];
export default WithLayout({ getLayout: null, getData: getProps, Page: LegacyPage })<"P">;
// @ts-expect-error TODO: fix this
export default WithLayout({ getLayout: null, getData, Page: LegacyPage })<"P">;
export const dynamic = "force-static";
// generate segments on demand
export const dynamicParams = true;
+6 -4
View File
@@ -9,7 +9,7 @@ import PageWrapper from "@components/PageWrapperAppDir";
type WithLayoutParams<T extends Record<string, any>> = {
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
Page?: (props: T) => React.ReactElement | null;
getData?: (arg: GetServerSidePropsContext) => Promise<T>;
getData?: (arg: GetServerSidePropsContext) => Promise<T | undefined>;
isBookingPage?: boolean;
};
@@ -22,9 +22,11 @@ export function WithLayout<T extends Record<string, any>>({
return async <P extends "P" | "L">(p: P extends "P" ? PageProps : LayoutProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const props = getData
? await getData(buildLegacyCtx(h, cookies(), p.params) as unknown as GetServerSidePropsContext)
: ({} as T);
let props = {} as T;
if ("searchParams" in p && getData) {
props = (await getData(buildLegacyCtx(h, cookies(), p.params, p.searchParams))) ?? ({} as T);
}
const children = "children" in p ? p.children : null;
+3 -3
View File
@@ -2,13 +2,13 @@ import NotFoundPage from "@pages/404";
import { WithLayout } from "app/layoutHOC";
import type { GetStaticPropsContext } from "next";
import { ssgInit } from "@server/lib/ssg";
import { getTranslations } from "@server/lib/getTranslations";
const getData = async (context: GetStaticPropsContext) => {
const ssg = await ssgInit(context);
const i18n = await getTranslations(context);
return {
dehydratedState: ssg.dehydrate(),
i18n,
};
};
+1 -2
View File
@@ -89,8 +89,7 @@ function PageWrapper(props: AppProps) {
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
),
router
)
)}
</AppProviders>
);
@@ -16,10 +16,10 @@ import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
isPending?: boolean;
}
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -42,7 +42,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
+18 -16
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
@@ -92,17 +92,19 @@ export const AppPage = ({
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
useEffect(
function refactorMeWithoutEffect() {
const data = appDbQuery.data;
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
[appDbQuery.data]
);
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
@@ -190,7 +192,7 @@ export const AppPage = ({
)}
</header>
</div>
{!appDbQuery.isLoading ? (
{!appDbQuery.isPending ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
@@ -211,7 +213,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -250,7 +252,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -272,7 +274,7 @@ export const AppPage = ({
)}
{dependencies &&
(!dependencyData.isLoading ? (
(!dependencyData.isPending ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
@@ -28,7 +28,7 @@ type Props = {
onChanged: () => unknown | Promise<unknown>;
fromOnboarding?: boolean;
destinationCalendarId?: string;
isLoading?: boolean;
isPending?: boolean;
};
function CalendarList(props: Props) {
@@ -74,7 +74,7 @@ function ConnectedCalendarsList(props: Props) {
suspense: true,
refetchOnWindowFocus: false,
});
const { fromOnboarding, isLoading } = props;
const { fromOnboarding, isPending } = props;
return (
<QueryCell
query={query}
@@ -97,7 +97,7 @@ function ConnectedCalendarsList(props: Props) {
<div className="flex flex-col xl:flex-row xl:space-x-5">
{!!data.connectedCalendars.length && (
<div className="flex items-center">
<AdditionalCalendarSelector isLoading={isLoading} />
<AdditionalCalendarSelector isPending={isPending} />
</div>
)}
</div>
@@ -229,7 +229,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
hidePlaceholder
value={data.destinationCalendar?.externalId}
onChange={mutation.mutate}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</div>
</div>
@@ -239,7 +239,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
onChanged={onChanged}
fromOnboarding={fromOnboarding}
destinationCalendarId={data.destinationCalendar?.externalId}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</>
)}
@@ -100,9 +100,9 @@ export const InstallAppButtonChild = ({
<DropdownMenuContent
className="w-auto"
onInteractOutside={(event) => {
if (mutation.isLoading) event.preventDefault();
if (mutation.isPending) event.preventDefault();
}}>
{mutation.isLoading && (
{mutation.isPending && (
<div className="z-1 fixed inset-0 flex items-center justify-center">
<Spinner />
</div>
@@ -139,7 +139,7 @@ function BookingListItem(booking: BookingItemProps) {
setRejectionDialogIsOpen(true);
},
icon: Ban,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !paymentAppData.enabled) ||
@@ -153,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
]
: []),
@@ -293,7 +293,7 @@ function BookingListItem(booking: BookingItemProps) {
setViewRecordingsDialogIsOpen(true);
},
color: showCheckRecordingButton ? "secondary" : "primary",
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
];
@@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
<DialogFooter>
<DialogClose />
<Button
disabled={mutation.isLoading}
disabled={mutation.isPending}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
disabled={chargeCardMutation.isPending || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
@@ -27,7 +27,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
async onSuccess() {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
@@ -65,7 +65,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={isLoading}
disabled={isPending}
onClick={() => {
rescheduleApi({
bookingId,
@@ -21,6 +21,7 @@ import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
@@ -35,7 +36,6 @@ import {
Tooltip,
} from "@calcom/ui";
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -55,8 +55,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
@@ -66,27 +66,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => {
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
eventType: formMethods.getValues("title"),
eventName: formMethods.getValues("eventName"),
host: formMethods.getValues("users")[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const [requiresConfirmation, setRequiresConfirmation] = useState(
formMethods.getValues("requiresConfirmation")
);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
const noShowFeeEnabled =
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]);
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
const toggleGuests = (enabled: boolean) => {
const bookingFields = formMethods.getValues("bookingFields");
@@ -108,7 +112,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -122,7 +126,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-4">
{/**
@@ -145,12 +148,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</Link>
</div>
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
@@ -167,7 +167,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
@@ -194,7 +193,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
},
}}
/>
@@ -203,15 +202,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
metadata={formMethods.getValues("metadata")}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
defaultValue={eventType.requiresBookerEmailVerification}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -228,8 +225,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="hideCalendarNotes"
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -246,7 +241,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="successRedirectUrl"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -274,7 +268,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div
@@ -327,7 +320,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Tooltip
content={
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
}>
<Button
color="minimal"
size="sm"
@@ -336,7 +332,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
if (formMethods.getValues("hashedLink")) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
@@ -353,8 +349,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -378,7 +372,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", 2);
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
@@ -388,8 +382,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div>
<TextField
@@ -399,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
defaultValue={value}
min={1}
containerClassName="max-w-80"
addOnSuffix={<>{t("seats")}</>}
@@ -408,21 +400,29 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
<div className="mt-4">
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<Controller
name="seatsShowAttendees"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) =>
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
<Controller
name="seatsShowAvailabilityCount"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
</div>
@@ -436,8 +436,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
control={formMethods.control}
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -452,59 +450,47 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{allowDisablingHostConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
setValue={(val: string) => formMethods.setValue("eventName", val)}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
+9 -10
View File
@@ -18,21 +18,21 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
extendsFeature: "EventType",
teamId: eventType.team?.id || eventType.parent?.teamId,
});
const methods = useFormContext<FormValues>();
const formMethods = useFormContext<FormValues>();
const installedApps =
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
const notInstalledApps =
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
const allAppsData = methods.watch("metadata")?.apps || {};
const allAppsData = formMethods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
methods.setValue("metadata", {
...methods.getValues("metadata"),
formMethods.setValue("metadata", {
...formMethods.getValues("metadata"),
apps: _allAppsData,
});
};
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const eventTypeFormMetadata = methods.getValues("metadata");
const eventTypeFormMetadata = formMethods.getValues("metadata");
const getAppDataSetter = (
appId: EventTypeAppsList,
@@ -56,8 +56,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
const appData = allAppsDataFromForm[appId];
setAllAppsData({
...allAppsDataFromForm,
@@ -138,7 +137,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
{!isPending && !installedApps?.length ? (
<EmptyScreen
Icon={Grid}
headline={t("empty_installed_apps_headline")}
@@ -179,7 +178,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
</div>
{!shouldLockDisableProps("apps").disabled && (
<div className="bg-muted mt-6 rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
{!isPending && notInstalledApps?.length ? (
<>
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
{t("available_apps_lower_case")}
@@ -85,7 +85,7 @@ const EventTypeScheduleDetails = memo(
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
const { isPending, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId:
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
@@ -112,7 +112,7 @@ const EventTypeScheduleDetails = memo(
)}>
{day}
</span>
{isLoading ? (
{isPending ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
@@ -142,7 +142,7 @@ const EventTypeScheduleDetails = memo(
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
disabled={isPending}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
@@ -169,8 +169,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const watchSchedule = watch("schedule");
const [options, setOptions] = useState<AvailabilityOption[]>([]);
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
onSuccess: ({ schedules }) => {
const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
useEffect(
function refactorMeWithoutEffect() {
if (!data) {
return;
}
const schedules = data.schedules;
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
@@ -225,8 +232,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
setValue("availability", value);
},
});
[data]
);
const availabilityValue = watch("availability");
useEffect(() => {
@@ -241,8 +248,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{t("availability")}
{shouldLockIndicator("availability")}
</label>
{isLoading && <SelectSkeletonLoader />}
{!isLoading && (
{isPending && <SelectSkeletonLoader />}
{!isPending && (
<Controller
name="schedule"
render={({ field }) => {
@@ -1,10 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { FormValues } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form";
import type { SingleValue } from "react-select";
import { classNames } from "@calcom/lib";
@@ -107,7 +107,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
export const EventLimitsTab = () => {
const { t, i18n } = useLocale();
const formMethods = useFormContext<FormValues>();
@@ -126,36 +126,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
},
];
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
startDate: new Date(eventType.periodStartDate || Date.now()),
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const watchPeriodType = useWatch({
control: formMethods.control,
name: "periodType",
defaultValue: periodType?.type,
});
const watchPeriodType = formMethods.watch("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
{ value: 1, label: t("calendar_days") },
];
// offsetStart toggle is client-side only, opened by default if offsetStart is set
const offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
// Preview how the offset will affect start times
const watchOffsetStartValue = formMethods.watch("offsetStart");
const offsetOriginalTime = new Date();
offsetOriginalTime.setHours(9, 0, 0, 0);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
return (
<div>
@@ -165,8 +149,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
@@ -197,8 +179,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
@@ -235,7 +215,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
@@ -254,8 +233,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
slotIntervalOptions.find(
(option) => option.value === formMethods.getValues("slotInterval")
) || slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
@@ -267,7 +247,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -300,7 +279,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="onlyShowFirstAvailableSlot"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value;
return (
@@ -323,7 +301,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -361,7 +338,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="periodType"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value && value !== "UNLIMITED";
@@ -380,7 +356,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
@@ -408,22 +383,23 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<Select
options={optionsPeriod}
isSearchable={false}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
onChange={(opt) =>
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
}
name="periodCoundCalendarDays"
value={optionsPeriod.find((opt) => {
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
})}
defaultValue={optionsPeriod.find(
(opt) =>
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
)}
/>
</div>
)}
@@ -431,8 +407,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
@@ -481,7 +455,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
containerClassName="max-w-80"
label={t("offset_start")}
{...formMethods.register("offsetStart")}
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
@@ -528,7 +502,10 @@ const IntervalLimitItem = ({
onIntervalSelect,
}: IntervalLimitItemProps) => {
return (
<div className="mb-4 flex max-h-9 items-center space-x-2 text-sm rtl:space-x-reverse" key={limitKey}>
<div
data-testid="add-limit"
className="mb-4 flex max-h-9 items-center space-x-2 text-sm rtl:space-x-reverse"
key={limitKey}>
<TextField
required
type="number"
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
};
+27 -42
View File
@@ -48,21 +48,19 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const getLocationInfo = ({
eventType,
locationOptions,
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
eventType.locations &&
eventType.locations.length > 0 &&
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
const locationDetails = eventType.locations &&
eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
name: eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setIsMounted(true);
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
editable={isEditable}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
@@ -113,7 +106,9 @@ export const EventSetupTab = (
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, team, destinationCalendar } = props;
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@@ -143,12 +138,12 @@ export const EventSetupTab = (
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
);
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -195,7 +190,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -219,7 +213,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -339,7 +332,7 @@ export const EventSetupTab = (
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
const fieldValues = formMethods.getValues().locations[index];
const fieldValues = formMethods.getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
displayLocationPublicly: e.target.checked,
@@ -408,7 +401,7 @@ export const EventSetupTab = (
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
@@ -463,7 +456,6 @@ export const EventSetupTab = (
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
@@ -471,23 +463,19 @@ export const EventSetupTab = (
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
{urlPrefix}/
{!isManagedEventType
? team
? (orgBranding ? "" : "team/") + team.slug
: eventType.users[0].username
: formMethods.getValues("users")[0].username
: t("username_placeholder")}
/
</>
@@ -564,7 +552,7 @@ export const EventSetupTab = (
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
defaultValue={formMethods.getValues("length") ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
min={1}
@@ -580,6 +568,8 @@ export const EventSetupTab = (
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
@@ -600,12 +590,7 @@ export const EventSetupTab = (
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
name="locations"
control={formMethods.control}
defaultValue={eventType.locations || []}
render={() => <Locations />}
/>
<Controller name="locations" render={() => <Locations />} />
</div>
</div>
</div>
@@ -67,55 +67,66 @@ type Props = {
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
activeWebhooksNumber: number;
};
function getNavigation(props: {
type getNavigationProps = {
t: TFunction;
eventType: Props["eventType"];
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
href: `/event-types/${id}?tabName=setup`,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
href: `/event-types/${id}?tabName=limits`,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
href: `/event-types/${id}?tabName=advanced`,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
href: `/event-types/${id}?tabName=recurring`,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
href: `/event-types/${id}?tabName=apps`,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
href: `/event-types/${id}?tabName=workflows`,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
@@ -136,6 +147,7 @@ function EventTypeSingleLayout({
availability,
isUserOrganizationAdmin,
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -145,7 +157,7 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED ||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@@ -167,16 +179,23 @@ function EventTypeSingleLayout({
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
t,
eventType,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -185,31 +204,29 @@ function EventTypeSingleLayout({
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: eventType.scheduleName ?? `default_schedule_name`,
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
: ""
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
@@ -225,15 +242,14 @@ function EventTypeSingleLayout({
}
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
return navigation;
}, [
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -241,16 +257,23 @@ function EventTypeSingleLayout({
isManagedEventType,
isChildrenManagedEventType,
team,
formMethods,
length,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
const permalink = `${bookerUrl}/${
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
}/${eventType.slug}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
const embedLink = `${
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
}/${formMethods.getValues("slug")}`;
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
// const title = formMethods.watch("title");
return (
<Shell
backPath="/event-types"
@@ -258,7 +281,7 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
{!eventType.metadata?.managedEventConfig && (
{!formMethods.getValues("metadata")?.managedEventConfig && (
<>
<div
className={classNames(
@@ -333,7 +356,7 @@ function EventTypeSingleLayout({
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={eventType.id}
eventId={formMethods.getValues("id")}
/>
</>
)}
@@ -446,14 +469,14 @@ function EventTypeSingleLayout({
</Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
deleteMutation.mutate({ id: formMethods.getValues("id") });
}}>
<p className="mt-5">
<Trans
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
setCreateModalOpen(false);
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -1,3 +1,4 @@
import type { Webhook } from "@prisma/client";
import { useSession } from "next-auth/react";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
@@ -5,10 +6,16 @@ import { useFormContext } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, EmptyScreen, SettingsToggle } from "@calcom/ui";
import { PhoneCall } from "@calcom/ui/components/icon";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
import { PhoneCall, Plus, Lock, Webhook as TbWebhook } from "@calcom/ui/components/icon";
type InstantEventControllerProps = {
eventType: EventTypeSetup;
@@ -82,11 +89,7 @@ export default function InstantEventController({
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
{instantEventState && (
<div data-testid="instant-event-collapsible" className="flex flex-col gap-2 text-sm">
<p>{t("warning_payment_instant_meeting_event")}</p>
</div>
)}
{instantEventState && <InstantMeetingWebhooks eventType={eventType} />}
</div>
</SettingsToggle>
</>
@@ -97,3 +100,201 @@ export default function InstantEventController({
</LicenseRequired>
);
}
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
eventTypeId: eventType.id,
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
});
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: true,
});
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
if (
subscriberUrlReserved({
subscriberUrl: values.subscriberUrl,
id: values.id,
webhooks,
eventTypeId: eventType.id,
})
) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
createWebhookMutation.mutate({
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
eventTypeId: eventType.id,
});
};
const NewWebhookButton = () => {
const { t } = useLocale();
return (
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={Plus}
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
);
};
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
return (
<div>
{webhooks && !isPending && (
<>
<div>
{webhooks.length ? (
<>
<div className="border-subtle my-2 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
canEditWebhook={!webhookLockedStatus.disabled}
onEditWebhook={() => {
setEditModalOpen(true);
setWebhookToEdit(webhook);
}}
/>
);
})}
</div>
<p className="text-default text-sm font-normal">
{t("warning_payment_instant_meeting_event")}
</p>
</>
) : (
<>
<p className="text-default mb-4 text-sm font-normal">
{t("warning_payment_instant_meeting_event")}
</p>
<EmptyScreen
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_instant_meeting_webhook_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<NewWebhookButton />
)
}
/>
</>
)}
</div>
{/* New webhook dialog */}
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
<DialogContent
enableOverflow
title={t("create_webhook")}
description={t("create_webhook_team_event_type")}>
<WebhookForm
noRoutingFormTriggers={true}
onSubmit={onCreateWebhook}
onCancel={() => setCreateModalOpen(false)}
apps={installedApps?.items.map((app) => app.slug)}
selectOnlyInstantMeetingOption={true}
/>
</DialogContent>
</Dialog>
{/* Edit webhook dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent enableOverflow title={t("edit_webhook")}>
<WebhookForm
noRoutingFormTriggers={true}
webhook={webhookToEdit}
apps={installedApps?.items.map((app) => app.slug)}
onCancel={() => setEditModalOpen(false)}
onSubmit={(values: WebhookFormSubmitData) => {
if (
subscriberUrlReserved({
subscriberUrl: values.subscriberUrl,
id: webhookToEdit?.id,
webhooks,
eventTypeId: eventType.id,
})
) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (values.changeSecret) {
values.secret = values.newSecret.length ? values.newSecret : null;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
editWebhookMutation.mutate({
id: webhookToEdit?.id || "",
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
eventTypeId: webhookToEdit?.eventTypeId || undefined,
});
}}
/>
</DialogContent>
</Dialog>
</>
)}
</div>
);
};
@@ -8,18 +8,18 @@ interface ManagedEventDialogProps {
slugExistsChildrenDialogOpen: ChildrenEventType[];
slug: string;
onOpenChange: () => void;
isLoading: boolean;
isPending: boolean;
onConfirm: (e: { preventDefault: () => void }) => void;
}
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
const { t } = useLocale();
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
return (
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
isLoading={isLoading}
isPending={isPending}
variety="warning"
title={t("managed_event_dialog_title", {
slug,
@@ -19,11 +19,10 @@ export default function RecurringEventController({
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
formMethods.getValues("recurringEvent")
);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
@@ -28,7 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
color="secondary"
disabled={installed}
type="button"
loading={buttonProps?.isLoading}
loading={buttonProps?.isPending}
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
@@ -31,7 +31,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
return (
<>
{/* Already connected calendars */}
{!queryConnectedCalendars.isLoading &&
{!queryConnectedCalendars.isPending &&
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryIntegrations.isLoading && <StepConnectionLoader />}
{queryIntegrations.isPending && <StepConnectionLoader />}
<button
type="button"
@@ -13,7 +13,7 @@ interface ConnectedAppStepProps {
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { nextStep } = props;
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
sortByMostPopular: true,
@@ -26,7 +26,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
return (
<>
{!isLoading && (
{!isPending && (
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
{queryConnectedVideoApps?.items &&
queryConnectedVideoApps?.items.map((item) => {
@@ -48,7 +48,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
</List>
)}
{isLoading && <StepConnectionLoader />}
{isPending && <StepConnectionLoader />}
<button
type="button"
data-testid="save-video-button"
@@ -113,7 +113,7 @@ const UserSettings = (props: IUserSettingsProps) => {
<Button
type="submit"
className="mt-8 flex w-full flex-row justify-center"
disabled={mutation.isLoading}>
disabled={mutation.isPending}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>

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