Compare commits

..
57 Commits
Author SHA1 Message Date
Keith Williams 4335c9ab92 v3.9.7
release.yml / release (push) Canceled after 0s
2024-04-09 09:37:32 -03:00
Peer Richelsen 20df7156cf Update common.json (#14472) 2024-04-09 12:23:54 +00:00
ErikandPeer Richelsen 9c35425e57 chore: Release SDK 1.0.0 (#14462)
* chore: dont require version

* fixup: type

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-04-09 11:56:02 +00:00
Hariom Balhara 65df5ac0e4 fix: Allow unpublished org to serve redirected links plus other org onboarding fixes (#14337)
* fix: Allow unpublished org to serve redirected links

* fix: Fix team event link for unpublished org

* fixes

* Rename variable

* Add minimum teams requirement for upgrade

* Fixes

* PR Feedback addressed
2024-04-09 10:33:39 +01:00
Keith Williams 84fdd03603 fix: Return team event type (#14464) 2024-04-08 18:35:41 -03:00
Crowdin Bot e6ec8bdf4d New Crowdin translations by Github Action 2024-04-08 19:17:45 +00:00
Yamini KandKeith Williams 86c8197104 docs: Update README.md regarding POSTGRES db port (#14431)
* Update README.md

Updating the description about POSTGRES DB port

* Update README.md

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-08 19:12:11 +00:00
Pat bb13badcf4 Update README.md (#14432)
Add minimum version number to PostgreSQL in 'Getting Started,' to help potential contributors successfully set up environment with local database.

I tried to set up local dev environment on Ubuntu 20.04 with PostgreSQL 12.18 which failed when running 'yarn workspace @calcom/prisma db-migrate'

PostgreSQL version 14.11 worked for me. I saw another dev say v. 13+ would work. Could save folks some troubleshooting time.

This is the error on PostgreSQL 12.18:

Error: P3006 Migration `20240321143215_move_avatars_cols_to_avatar_table` failed to apply cleanly to the shadow database. Error: ERROR: function gen_random_uuid() does not exist HINT: No function matches the given name and argument types. You might need to add explicit type casts. 0: sql_schema_connector::validate_migrations with namespaces=None at schema-engine/connectors/sql-schema-connector/src/lib.rs:324 1: schema_core::state::DevDiagnostic at schema-engine/core/src/state.rs:270
2024-04-08 16:09:43 -03:00
Keith Williams 64b2397fa2 v3.9.6
release.yml / release (push) Canceled after 0s
2024-04-08 12:42:15 -03:00
Kartik SainiandUdit Takkar 90130457e2 fix: Add missing import for Icon component in MultiDropdownSelect.tsx (#14440)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-08 13:44:47 +00:00
Amit Sharma 260521fb28 fix: Empty "Add guests" email input is blocking form submission (#14442) 2024-04-08 13:24:15 +00:00
Hariom Balhara 6390d595f5 fix: Team Invite Email Text formatting (#14434) 2024-04-08 10:15:26 +00:00
Udit Takkar 74dc46f8b2 chore: change timezone format and update webhook (#14433)
* chore: change timezone format

* chore: set guest email and guest company to null

* feat: also check states to get id

* chore: improve code

* chore: fix type erro

* chore: update zod schema
2024-04-08 15:26:31 +05:30
Lauris Skraucis 67262910cd fix: v1 tsconfig decorators (#14454) 2024-04-08 11:51:56 +03:00
Omar López d59e68de6d ci: adds api v1 builds to required checks (#14345) 2024-04-05 23:00:01 +00:00
zomars b50318fbca chore: Adds devops script for getting table sizes 2024-04-05 14:29:50 -07:00
Udit Takkar 1ce7df324d feat: retell AI webhook url (#14430)
* feat: retell AI webhook url

* fix: type error

* chore

* chore: make company and email optional

* chore: remove logs

* chore: type error

* chore: type error

* fix: type err

* fix: pass guest email

* fix: type err

* chore: change date format

* chore: move advanced format
2024-04-05 22:44:57 +05:30
sean-brydonandKeith Williams e3b7264208 fix: filter and listMember handler (#14429)
* fix: filter and listMember handler

* Update packages/trpc/server/routers/viewer/teams/listMembers.handler.ts

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* Update packages/features/bookings/components/PeopleFilter.tsx

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-05 11:51:22 -03:00
Morgan 5e3f6e6c2c chore: bump atoms package version (#14428) 2024-04-05 14:00:22 +00:00
Udit Takkar 5b6c43118e fix: change identity provider on password reset (#14326)
* fix: change identity provider on password reset

* chore: remove check
2024-04-05 10:13:43 +00:00
Amit SharmaandHariom beee545568 fix: "Checkbox" custom field throws error (#14339)
* fix: "Checkbox" custom field throws error

* Add e2e

* Self review feedback

* Fix typo

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-04-05 15:05:48 +05:30
Lauris SkraucisandKeith Williams a597defd03 fix: create platform handler (#14288)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-04-05 07:46:29 +00:00
Morgan e3562cdfb5 fix: scope managed user to client id (#14347) 2024-04-05 09:14:24 +02:00
Morgan 86a7824ec9 fix: remove port in prod url apiv2 (#14343) 2024-04-04 20:25:35 +00:00
Morgan 7001515a44 chore: bump atoms package to 1.0.11 (#14344) 2024-04-04 19:31:45 +00:00
zomars 36044c4ab3 ci: enable settings for app-router canary 2024-04-04 11:53:40 -07:00
Omar López 4933295810 v3.9.5
release.yml / release (push) Canceled after 0s
2024-04-04 11:34:47 -07:00
Carina WollendorferandCarinaWolli ed0cd30092 fix: improve api/cron/webhookTriggers endpoint (#14188)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-04-04 10:01:57 -07:00
Alex van Andel 1132d9d0dc chore: teams filter improvements (#14315)
* Changed translations, many languages were wrong

* Clear localStorage from the useTypedQuery

* Missed one
2024-04-04 09:59:58 -07:00
9bd3a362a2 feat: added Rating Template to Workflows (#14215)
Co-authored-by: alishaz-polymath <alishahbaz7@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2024-04-04 09:58:52 -07:00
Lauris SkraucisandMorgan 844076badb docs: v2 event types swagger (#14296)
* docs: GET /event-types/:id response

* docs: GET /event-types response

* docs: UPDATE /event-types/:id response

* docs: DELETE /event-types/:id response

* docs: GET /event-types/:username/public response

* docs: prod docs show only event-types, bookings cancel and reschedule, managed users

* fix merge main

* Revert "docs: prod docs show only event-types, bookings cancel and reschedule, managed users"

This reverts commit 96cdc4b4840e3841c6ed27bd3a030d0574aad19e.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-04-04 16:20:03 +00:00
Lauris Skraucis c0523a31a7 Revert "refactor: OAuth creation form hide apps permissions (#14264)" (#14336)
This reverts commit fe62f9c6de.
2024-04-04 15:24:05 +00:00
Morgan 1fff642860 fix: urls, gcal check, oauth client controller (#14335)
* fix: urls, gcal check, oauth client controller

* fixup! fix: urls, gcal check, oauth client controller
2024-04-04 14:58:15 +00:00
89fe383c9f feat: implement turnstile on signup (#13913)
* wip

* feat: turnstile + removal of trpcState

* fix: use react-turnstile

* fix: i18n hydration errors

* wip: server side validation for cf token

* fix: hide in e2e tests

* feat:throw on error of token

* fix: set correct form values onVerify

* fix: use getIp for remoteIp and use correct header for token input

* chore: update .env.example

* fix: Update .env.example

* Update apps/web/pages/signup.tsx

* chore: fix typo

* fix: typo

* feat: hide until error

* feat: appearance interaction only

* remove cloudflare from e2e

* fix: legacyBehavior

* fix links + wait for load

* fix: signup tests

* test: wait for load state

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-04 10:21:06 -03:00
sean-brydon 4a173c48b8 fix: correct limit amount in api middleware (#14333) 2024-04-04 12:20:26 +00:00
Morgan a72a3e45e9 fix: refresh access token expiry 60m (#14331)
* fix: refresh access token expiry 60m

* fix: exception filters logging request

* fix: never auto accept domain for managed users
2024-04-04 09:07:43 -03:00
a8d8c992a4 chore: added email and company form to cal.ai form (#14306)
* added email and company form

* i18n

* feat: add guest email and company

* chore: update and format prompt

* chore: hint var

* chore: fix err message

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-04-04 11:57:43 +00:00
Hariom Balhara 99f9a121cb fix: Show proper error when API_V2_URL variable is missing (#14269) 2024-04-04 11:51:39 +00:00
Crowdin Bot 6a0e2392fe New Crowdin translations by Github Action 2024-04-04 11:49:00 +00:00
Joe Au-Yeung addec46feb chore: Remove payment keys from appKeysSchema (#14298)
* Remove payment keys from appKeysSchema

* Update .env.example
2024-04-04 11:44:16 +00:00
Crowdin Bot a4fe9a51b5 New Crowdin translations by Github Action 2024-04-04 11:41:42 +00:00
Richard Poelderl 78b8bbb5aa update docs for yarn.lock management (#14230) 2024-04-04 08:37:15 -03:00
Lauris Skraucis d7ece4f583 Revert "chore: downgrade react query atoms (#14328)" (#14332)
This reverts commit 8fc9b9ae9d.
2024-04-04 10:55:15 +00:00
Lauris Skraucis 8fc9b9ae9d chore: downgrade react query atoms (#14328)
* refactor: downgrade atoms react-query to v4

* driveby: prettier format file

* refactor: react-query isPending to isLoading

* chore: bump atoms version

* refactor: isPending -> isLoading

* chore: bump version
2024-04-04 11:28:35 +01:00
Udit Takkar d1175bf7de fix: pass begin_message for updating LLM (#14327) 2024-04-04 11:14:19 +01:00
Crowdin Bot 3900453482 New Crowdin translations by Github Action 2024-04-04 09:46:04 +00:00
Morganandsupalarry 68e7e76b31 feat: list managed users of an oauth client (#14330)
* feat: list managed users with apiv2 api

* refactor: isPending -> isLoading

* Revert "refactor: isPending -> isLoading"

This reverts commit dc2108e05508070185d57633346f3c532458e2a8.

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
2024-04-04 09:41:47 +00:00
zomars 986d196a37 hotfix: lazy icon dev performance 2024-04-03 22:14:30 -07:00
d30fb213d2 fix: Add adminReview option to allow impersonating org members (#14275)
* added layouts for moving teams step

* added admin section for org creation step

* extended org creation to also move existing teams

* wip

* wip

* wip

* further changes

* Add checkout for org in onboarding

* Fix ts errors

* Self review feedback

* Self review addressed

* Fix unit tests

* Fix ts error

* Seans feedback addressed

* feat: fix correct accounts pending

* fix: unit tests for new invite member permissions

* tests: org admin onboarding tests for existing user

* tests: Inital user self serve flow

* chore: update teamAndUserFixture to create X amount of teams

* chore: add testId to card actionButton

* test: add test-Id to continue or checkout button

* tests: add tests for migrating existing teams

* feat: match new designs

* fix: isAdminCheck

* chore: fix pricing copy

* fix: flacky tests

* Fix tests?

* More test fixes

* Check all checkboxes

* Fix type error

* Fix failing test and typescript issues

* Fix unpaid org allowing auto-add users

* Add self-serve flag

* Skip tests

* Add adminReview option

* Fixes

* Add server checks for impersonation

* Improved error

* feat: extract - and disalow updating from org uset list if not reviewed

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-04-04 04:48:41 +00:00
3f02de8214 chore: icon cleanup (#13770)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2024-04-03 23:23:57 +00:00
Omar López 7421cea0da fixes: API builds after rename (#14319) 2024-04-04 00:12:09 +01:00
Lauris Skraucis 507daa6a0c chore: publish atoms on npm and bump version (#14309) 2024-04-03 18:37:00 +03:00
Crowdin Bot 61f68af435 New Crowdin translations by Github Action 2024-04-03 13:32:23 +00:00
b3f4cfab32 feat: new org onboarding wizard (#13139)
* added layouts for moving teams step

* added admin section for org creation step

* extended org creation to also move existing teams

* wip

* wip

* wip

* further changes

* Add checkout for org in onboarding

* Fix ts errors

* Self review feedback

* Self review addressed

* Fix unit tests

* Fix ts error

* Seans feedback addressed

* feat: fix correct accounts pending

* fix: unit tests for new invite member permissions

* tests: org admin onboarding tests for existing user

* tests: Inital user self serve flow

* chore: update teamAndUserFixture to create X amount of teams

* chore: add testId to card actionButton

* test: add test-Id to continue or checkout button

* tests: add tests for migrating existing teams

* feat: match new designs

* fix: isAdminCheck

* chore: fix pricing copy

* fix: flacky tests

* Fix tests?

* More test fixes

* Check all checkboxes

* Fix type error

* Fix failing test and typescript issues

* Fix unpaid org allowing auto-add users

* Add self-serve flag

* Skip tests

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2024-04-03 18:53:02 +05:30
Morgan c3b1de498d fix: org guard did not have user org profile + missing db read in selectedCalendarRepo (#14303)
* fix: user did not contain profile with next-auth-guard

* fix: missing dbread instance
2024-04-03 10:49:19 +03:00
Erik cdf059361e chore: Tidy up unused variables, etc (#14289) 2024-04-02 22:06:42 +00:00
Alex van AndelandUdit Takkar 2aada9a7da chore: Move avatars using cursor (#14257)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-04-02 12:59:39 -07:00
504 changed files with 7660 additions and 3375 deletions
+5 -2
View File
@@ -169,8 +169,7 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID=
PAYMENT_FEE_FIXED=
PAYMENT_FEE_PERCENTAGE=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
@@ -213,6 +212,10 @@ E2E_TEST_MAILHOG_ENABLED=
# **********************************************************************************************************
# Cloudflare Turnstile
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
CLOUDFLARE_TURNSTILE_SECRET=
# Set the following value to true if you wish to enable Team Impersonation
NEXT_PUBLIC_TEAM_IMPERSONATION=false
+1
View File
@@ -4,3 +4,4 @@ node_modules
**/**/public
packages/prisma/zod
apps/web/public/embed
packages/ui/components/icon/dynamicIconImports.tsx
@@ -0,0 +1,69 @@
name: Production Build
on:
workflow_call:
env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
jobs:
build:
name: Build API V1
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
services:
postgres:
image: postgres:13
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: calendso
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn turbo run build --filter=@calcom/api...
shell: bash
+8 -1
View File
@@ -56,6 +56,13 @@ jobs:
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-api-v1:
name: Production build
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/api-v1-production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
needs: [changes]
@@ -99,7 +106,7 @@ jobs:
secrets: inherit
required:
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
needs: [changes, lint, type-check, test, build, build-api-v1, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
if: always()
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
+1
View File
@@ -17,3 +17,4 @@ packages/prisma/zod
packages/prisma/enums
apps/web/public/embed
apps/api/v2/swagger/documentation.json
packages/ui/components/icon/dynamicIconImports.tsx
+13 -2
View File
@@ -13,8 +13,6 @@ Contributions are what makes the open source community such an amazing place to
- Issues from non-core members automatically receive the `🚨 needs approval` label.
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
## Priorities
<table>
@@ -178,11 +176,15 @@ If you get errors, be sure to fix them before committing.
Do not commit your `yarn.lock` unless you've made changes to the `package.json`. If you've already committed `yarn.lock` unintentionally, follow these steps to undo:
If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`:
```bash
git checkout HEAD~1 yarn.lock
git commit -m "Revert yarn.lock changes"
```
_NB_: You may have to bypass the pre-commit hook with by appending `--no-verify` to the git commit
If you've pushed the commit with the `yarn.lock`:
1. Correct the commit locally using the above method.
2. Carefully force push:
@@ -194,26 +196,35 @@ If `yarn.lock` was committed a while ago and there have been several commits sin
1. **Checkout a Previous Version**:
- Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log:
```bash
git log yarn.lock
```
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
```bash
git checkout <commit_hash> yarn.lock
```
2. **Commit the Reverted Version**:
- After checking out the previous version of the `yarn.lock`, commit this change:
```bash
git commit -m "Revert yarn.lock to its state before unintended changes"
```
3. **Proceed with Caution**:
- If you need to push this change, first pull the latest changes from your remote branch to ensure you're not overwriting other recent changes:
```bash
git pull origin <your-branch-name>
```
- Then push the updated branch:
```bash
git push origin <your-branch-name>
```
Lastly, make sure to keep the branches updated (e.g. click the `Update branch` button on GitHub PR).
+2 -2
View File
@@ -113,7 +113,7 @@ To get a local copy up and running, please follow these simple steps.
Here is what you need to be able to run Cal.com.
- Node.js (Version: >=18.x)
- PostgreSQL
- PostgreSQL (Version: >=13.x)
- Yarn _(recommended)_
> If you want to enable any of the available integrations, you may want to obtain additional credentials for each one. More details on this can be found below under the [integrations section](#integrations).
@@ -210,7 +210,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
![image](https://user-images.githubusercontent.com/39329182/236612291-51d87f69-6dc1-4a23-bf4d-1ca1754e0a35.png)
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`. The port is configurable and does not have to be 5432.
</details>
+1 -2
View File
@@ -1,7 +1,6 @@
import type { NextMiddleware } from "next-api-middleware";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
@@ -11,7 +10,7 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
identifier: req.query.apiKey as string,
rateLimitingType: "api",
onRateLimiterResponse: (response) => {
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
res.setHeader("X-RateLimit-Limit", response.limit);
res.setHeader("X-RateLimit-Remaining", response.remaining);
res.setHeader("X-RateLimit-Reset", response.reset);
},
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
await canAccessTeamEventOrThrow(req, {
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
});
return true;
}
if (eventType?.userId === req.userId) return true; // is owner.
throw new HttpError({ statusCode: 403, message: "Forbidden" });
@@ -1,7 +1,7 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { purchaseTeamSubscription } from "@calcom/features/ee/teams/lib/payments";
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
@@ -105,10 +105,11 @@ export async function patchHandler(req: NextApiRequest) {
};
delete data.slug;
if (IS_TEAM_BILLING_ENABLED) {
const checkoutSession = await purchaseTeamSubscription({
const checkoutSession = await purchaseTeamOrOrgSubscription({
teamId: _team.id,
seats: _team.members.length,
userId,
pricePerSeat: null,
});
if (!checkoutSession.url)
throw new TRPCError({
+3 -1
View File
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
@@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
customer_update: {
address: "auto",
},
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
automatic_tax: {
enabled: true,
enabled: IS_PRODUCTION,
},
metadata: {
pendingPaymentTeamId,
+2 -1
View File
@@ -7,7 +7,8 @@
"paths": {
"~/*": ["*"],
"@prisma/client/*": ["@calcom/prisma/client/*"]
}
},
"experimentalDecorators": true
},
"include": [
"next-env.d.ts",
+1 -1
View File
@@ -8,7 +8,7 @@ export class AppLoggerMiddleware implements NestMiddleware {
private logger = new Logger("HTTP");
use(request: Request, response: Response, next: NextFunction): void {
const { ip, method, protocol, originalUrl, path: url } = request;
const { ip, method, originalUrl } = request;
const userAgent = request.get("user-agent") || "";
response.on("close", () => {
+3 -1
View File
@@ -11,7 +11,9 @@ const loadConfig = (): AppConfig => {
port: Number(getEnv("API_PORT", "5555")),
path: getEnv("API_URL", "http://localhost"),
url: `${getEnv("API_URL", "http://localhost")}${
process.env.API_PORT ? `:${Number(getEnv("API_PORT", "5555"))}` : ""
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
? `:${Number(getEnv("API_PORT", "5555"))}`
: ""
}/api/v2`,
},
db: {
@@ -1,5 +1,5 @@
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
import { IsBoolean, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
@@ -2,7 +2,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Controller, Get, Logger, UseGuards, Query } from "@nestjs/common";
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
@@ -16,8 +16,6 @@ import { EventBusyDate } from "@calcom/types/Calendar";
})
@UseGuards(AccessTokenGuard)
export class CalendarsController {
private readonly logger = new Logger("ee overlay calendars controller");
constructor(private readonly calendarsService: CalendarsService) {}
@Get("/busy-times")
@@ -1,8 +1,12 @@
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
@@ -22,17 +26,9 @@ import {
Delete,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { EventType } from "@prisma/client";
import { EventTypesByViewer } from "@calcom/lib";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import type {
EventType as AtomEventType,
EventTypesPublic,
UpdateEventTypeReturn,
} from "@calcom/platform-libraries";
import { getEventTypesByViewer } from "@calcom/platform-libraries";
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
@Controller({
path: "event-types",
@@ -63,12 +59,9 @@ export class EventTypesController {
@UseGuards(AccessTokenGuard)
async getEventType(
@Param("eventTypeId") eventTypeId: string,
@ForAtom() forAtom: boolean,
@GetUser() user: UserWithProfile
): Promise<ApiSuccessResponse<EventType | AtomEventType>> {
const eventType = forAtom
? await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId))
: await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
): Promise<GetEventTypeOutput> {
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
@@ -83,7 +76,7 @@ export class EventTypesController {
@Get("/")
@Permissions([EVENT_TYPE_READ])
@UseGuards(AccessTokenGuard)
async getEventTypes(@GetUser() user: UserWithProfile): Promise<ApiSuccessResponse<EventTypesByViewer>> {
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
const eventTypes = await getEventTypesByViewer({
id: user.id,
profile: {
@@ -99,9 +92,7 @@ export class EventTypesController {
@Get("/:username/public")
@Permissions([EVENT_TYPE_READ])
async getPublicEventTypes(
@Param("username") username: string
): Promise<ApiSuccessResponse<EventTypesPublic>> {
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
return {
@@ -118,7 +109,7 @@ export class EventTypesController {
@Param("eventTypeId") eventTypeId: number,
@Body() body: UpdateEventTypeInput,
@GetUser() user: UserWithProfile
): Promise<ApiResponse<UpdateEventTypeReturn["eventType"]>> {
): Promise<UpdateEventTypeOutput> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
return {
@@ -133,12 +124,17 @@ export class EventTypesController {
async deleteEventType(
@Param("eventTypeId") eventTypeId: number,
@GetUser("id") userId: number
): Promise<ApiResponse<EventType>> {
): Promise<DeleteEventTypeOutput> {
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
return {
status: SUCCESS_STATUS,
data: eventType,
data: {
id: eventType.id,
length: eventType.length,
slug: eventType.slug,
title: eventType.title,
},
};
}
}
@@ -0,0 +1,38 @@
import {
CREATE_EVENT_LENGTH_EXAMPLE,
CREATE_EVENT_SLUG_EXAMPLE,
CREATE_EVENT_TITLE_EXAMPLE,
} from "@/ee/event-types/inputs/create-event-type.input";
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsInt, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class DeleteData {
@IsInt()
@DocsProperty({ example: 1 })
id!: number;
@IsInt()
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
length!: number;
@IsString()
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
slug!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title!: string;
}
export class DeleteEventTypeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Type(() => DeleteData)
data!: DeleteData;
}
@@ -0,0 +1,28 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class Data {
@ApiProperty({
type: EventTypeOutput,
})
@ValidateNested()
@Type(() => EventTypeOutput)
eventType!: EventTypeOutput;
}
export class GetEventTypeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: Data,
})
@ValidateNested()
@Type(() => Data)
data!: Data;
}
@@ -0,0 +1,43 @@
import {
CREATE_EVENT_LENGTH_EXAMPLE,
CREATE_EVENT_SLUG_EXAMPLE,
CREATE_EVENT_TITLE_EXAMPLE,
} from "@/ee/event-types/inputs/create-event-type.input";
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class PublicEventType {
@IsInt()
@DocsProperty({ example: 1 })
id!: number;
@IsInt()
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
length!: number;
@IsString()
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
slug!: string;
@IsString()
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title!: string;
@IsString()
description?: string | null;
}
export class GetEventTypesPublicOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested({ each: true })
@Type(() => PublicEventType)
@IsArray()
data!: PublicEventType[];
}
@@ -0,0 +1,31 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEnum, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
class EventTypeGroup {
@ValidateNested({ each: true })
@Type(() => EventTypeOutput)
@IsArray()
eventTypes!: EventTypeOutput[];
}
class GetEventTypesData {
@ValidateNested({ each: true })
@Type(() => EventTypeGroup)
@IsArray()
eventTypeGroups!: EventTypeGroup[];
}
export class GetEventTypesOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested({ each: true })
@Type(() => GetEventTypesData)
@IsArray()
data!: GetEventTypesData;
}
@@ -0,0 +1,20 @@
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class UpdateEventTypeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
type: EventTypeOutput,
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => EventTypeOutput)
data!: EventTypeOutput;
}
@@ -101,11 +101,7 @@ export class EventTypesService {
}
async createUserDefaultEventTypes(userId: number) {
const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes;
const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo;
const sixtyMinutes = DEFAULT_EVENT_TYPES.sixtyMinutes;
const sixtyMinutesVideo = DEFAULT_EVENT_TYPES.sixtyMinutesVideo;
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
const defaultEventTypes = await Promise.all([
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
@@ -120,7 +116,7 @@ export class EventTypesService {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const { eventType } = await updateEventType({
await updateEventType({
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
@@ -130,6 +126,8 @@ export class EventTypesService {
},
});
const { eventType } = await this.getUserEventTypeForAtom(user, eventTypeId);
return eventType;
}
@@ -32,6 +32,7 @@ describe("Platform Gcal Endpoints", () => {
let gcalCredentials: Credential;
let accessTokenSecret: string;
let refreshTokenSecret: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
+2 -4
View File
@@ -1,5 +1,4 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GcalService } from "@/modules/apps/services/gcal.service";
import { GCalService } from "@/modules/apps/services/gcal.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
@@ -47,12 +46,11 @@ export class GcalController {
private readonly logger = new Logger("Platform Gcal Provider");
constructor(
private readonly appRepository: AppsRepository,
private readonly credentialRepository: CredentialsRepository,
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly config: ConfigService,
private readonly gcalService: GcalService
private readonly gcalService: GCalService
) {}
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
+2 -2
View File
@@ -1,6 +1,6 @@
import { GcalController } from "@/ee/gcal/gcal.controller";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GcalService } from "@/modules/apps/services/gcal.service";
import { GCalService } from "@/modules/apps/services/gcal.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
@@ -11,7 +11,7 @@ import { ConfigService } from "@nestjs/config";
@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
controllers: [GcalController],
})
export class GcalModule {}
@@ -1,7 +1,6 @@
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
BadRequestException,
@@ -9,7 +8,6 @@ import {
Get,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Param,
UnauthorizedException,
@@ -24,12 +22,7 @@ import { ApiResponse } from "@calcom/platform-types";
version: "2",
})
export class CalProviderController {
private readonly logger = new Logger("Platform Provider Controller");
constructor(
private readonly tokensRepository: TokensRepository,
private readonly oauthClientRepository: OAuthClientRepository
) {}
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
@Get("/:clientId")
@HttpCode(HttpStatus.OK)
@@ -1,4 +1,5 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import { Response } from "@calcom/platform-types";
@@ -10,11 +11,14 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
request,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
});
response.status(statusCode).json({
status: ERROR_STATUS,
@@ -7,6 +7,7 @@ import {
PrismaClientUnknownRequestError,
PrismaClientValidationError,
} from "@prisma/client/runtime/library";
import { Request } from "express";
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
import { Response } from "@calcom/platform-types";
@@ -31,10 +32,13 @@ export class PrismaExceptionFilter implements ExceptionFilter {
catch(error: PrismaError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const request = ctx.getRequest<Request>();
this.logger.error(`PrismaError: ${error.message}`, {
error,
request,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
});
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
status: ERROR_STATUS,
@@ -1,6 +1,7 @@
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";
import * as Sentry from "@sentry/node";
import { Request } from "express";
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
import { Response } from "@calcom/platform-types";
@@ -12,11 +13,14 @@ export class SentryFilter extends BaseExceptionFilter {
handleUnknownError(exception: any, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const request = ctx.getRequest<Request>();
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
exception,
request,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
});
// capture if client has been init
@@ -1,4 +1,5 @@
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import { TRPCError } from "@calcom/platform-libraries";
@@ -11,7 +12,8 @@ export class TRPCExceptionFilter implements ExceptionFilter {
catch(exception: TRPCError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const request = ctx.getRequest<Request>();
let statusCode = 500;
switch (exception.code) {
case "UNAUTHORIZED":
@@ -38,10 +40,15 @@ export class TRPCExceptionFilter implements ExceptionFilter {
statusCode = 500;
break;
}
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
exception,
request,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
});
response.status(statusCode).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
@@ -1,5 +1,6 @@
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
import { Catch, HttpStatus, Logger } from "@nestjs/common";
import { Request } from "express";
import { ZodError } from "zod";
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
@@ -12,11 +13,16 @@ export class ZodExceptionFilter implements ExceptionFilter {
catch(error: ZodError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
const request = ctx.getRequest<Request>();
this.logger.error(`ZodError: ${error.message}`, {
error,
request,
body: request.body,
headers: request.headers,
url: request.url,
method: request.method,
});
response.status(HttpStatus.BAD_REQUEST).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
@@ -1,6 +1,6 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const ForAtom = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
export const ForAtom = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.query.for === "atom";
});
@@ -1,4 +1,11 @@
import { UserWithProfile } from "@/modules/users/users.repository";
export class BaseStrategy {
success!: (user: unknown) => void;
error!: (error: Error) => void;
}
export class NextAuthPassportStrategy {
success!: (user: UserWithProfile) => void;
error!: (error: Error) => void;
}
@@ -4,9 +4,11 @@ import { google } from "googleapis";
import { z } from "zod";
@Injectable()
export class GcalService {
export class GCalService {
private logger = new Logger("GcalService");
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
constructor(private readonly appsRepository: AppsRepository) {}
async getOAuthClient(redirectUri: string) {
@@ -17,9 +19,7 @@ export class GcalService {
throw new NotFoundException();
}
const { client_id, client_secret } = z
.object({ client_id: z.string(), client_secret: z.string() })
.parse(app.keys);
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
return oAuth2Client;
@@ -38,7 +38,7 @@ export class OrganizationRolesGuard implements CanActivate {
async isPlatform(organizationId: number) {
const isPlatform = await this.organizationsService.isPlatform(organizationId);
if (!isPlatform) {
throw new ForbiddenException("Organization is not a platform.");
throw new ForbiddenException("Organization is not a platform (SHP).");
}
}
@@ -1,4 +1,4 @@
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@@ -7,7 +7,7 @@ import type { Request } from "express";
import { getToken } from "next-auth/jwt";
@Injectable()
export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
super();
}
@@ -25,7 +25,7 @@ export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth"
throw new UnauthorizedException("Email not found in the authentication token.");
}
const user = await this.userRepository.findByEmail(payload.email);
const user = await this.userRepository.findByEmailWithProfile(payload.email);
if (!user) {
throw new UnauthorizedException("User associated with the authentication token email not found.");
}
@@ -22,7 +22,10 @@ export class EventsController {
queryParams.eventSlug,
queryParams.isTeamEvent,
queryParams.org || null,
this.prismaReadService.prisma as unknown as PrismaClient
this.prismaReadService.prisma as unknown as PrismaClient,
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
true
);
return {
data: event,
@@ -179,6 +179,21 @@ describe("OAuth Client Users Endpoints", () => {
).toBeTruthy();
}
it(`/GET: return list of managed users`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`)
.set("x-cal-secret-key", oAuthClient.secret)
.set("Origin", `${CLIENT_REDIRECT_URI}`)
.expect(200);
const responseBody: ApiSuccessResponse<UserReturned[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toBeGreaterThan(0);
expect(responseBody.data[0].email).toEqual(postResponseData.user.email);
});
it(`/GET/:id`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
@@ -20,11 +20,12 @@ import {
Patch,
BadRequestException,
Delete,
Query,
} from "@nestjs/common";
import { User } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
import { ApiResponse, Pagination } from "@calcom/platform-types";
@Controller({
path: "oauth-clients/:clientId/users",
@@ -39,6 +40,27 @@ export class OAuthClientUsersController {
private readonly oauthRepository: OAuthClientRepository
) {}
@Get("/")
@UseGuards(OAuthClientCredentialsGuard)
async getManagedUsers(
@Param("clientId") oAuthClientId: string,
@Query() queryParams: Pagination
): Promise<ApiResponse<User[]>> {
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
const { offset, limit } = queryParams;
const existingUsers = await this.userRepository.findManagedUsersByOAuthClientId(
oAuthClientId,
offset ?? 0,
limit ?? 50
);
return {
status: SUCCESS_STATUS,
data: existingUsers,
};
}
@Post("/")
@UseGuards(OAuthClientCredentialsGuard)
async createUser(
@@ -8,6 +8,7 @@ import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/o
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Body,
Controller,
@@ -57,9 +58,10 @@ export class OAuthClientsController {
type: CreateOAuthClientResponseDto,
})
async createOAuthClient(
@GetUser("organizationId") organizationId: number,
@GetUser() user: UserWithProfile,
@Body() body: CreateOAuthClientInput
): Promise<CreateOAuthClientResponseDto> {
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
this.logger.log(
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
);
@@ -77,9 +79,9 @@ export class OAuthClientsController {
@HttpCode(HttpStatus.OK)
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@DocsOperation({ description: AUTH_DOCUMENTATION })
async getOAuthClients(
@GetUser("organizationId") organizationId: number
): Promise<GetOAuthClientsResponseDto> {
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
return { status: SUCCESS_STATUS, data: clients };
}
@@ -2,7 +2,7 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { BadRequestException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import * as crypto from "crypto";
@@ -24,24 +24,24 @@ export class OAuthClientUsersService {
) {
let user: User;
if (!organizationId) {
const username = generateShortHash(body.email, oAuthClientId);
user = await this.userRepository.create(body, username, oAuthClientId, isPlatformManaged);
throw new BadRequestException("You cannot create a managed user outside of an organization");
} else {
const [_, emailDomain] = body.email.split("@");
const [username, emailDomain] = body.email.split("@");
const email = `${username}+${oAuthClientId}@${emailDomain}`;
user = (
await createNewUsersConnectToOrgIfExists({
usernamesOrEmails: [body.email],
usernamesOrEmails: [email],
input: {
teamId: organizationId,
role: "MEMBER",
usernameOrEmail: [body.email],
usernameOrEmail: [email],
isOrg: true,
language: "en",
},
parentId: null,
autoAcceptEmailDomain: emailDomain,
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
connectionInfoMap: {
[body.email]: {
[email]: {
orgId: organizationId,
autoAccept: true,
},
@@ -1,20 +1,18 @@
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
@Injectable()
export class OAuthFlowService {
private logger = new Logger("OAuthFlowService");
constructor(
private readonly tokensRepository: TokensRepository,
private readonly oAuthClientRepository: OAuthClientRepository
) //private readonly redisService: RedisIOService
{}
private readonly oAuthClientRepository: OAuthClientRepository //private readonly redisService: RedisIOService
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async propagateAccessToken(accessToken: string) {
// this.logger.log("Propagating access token to redis", accessToken);
// TODO propagate
@@ -48,6 +46,7 @@ export class OAuthFlowService {
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async readFromCache(secret: string) {
return { status: "CACHE_MISS" };
}
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
export class PrismaReadService implements OnModuleInit {
public prisma: PrismaClient;
constructor(private readonly configService: ConfigService) {
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.readUrl", { infer: true });
this.prisma = new PrismaClient({
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
export class PrismaWriteService implements OnModuleInit {
public prisma: PrismaClient;
constructor(private readonly configService: ConfigService) {
constructor(readonly configService: ConfigService) {
const dbUrl = configService.get("db.writeUrl", { infer: true });
this.prisma = new PrismaClient({
@@ -101,7 +101,7 @@ export class TokensRepository {
}
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -83,6 +83,17 @@ export class UsersRepository {
});
}
async findByEmailWithProfile(email: string) {
return this.dbRead.prisma.user.findUnique({
where: {
email,
},
include: {
movedToProfile: true,
},
});
}
async findByUsername(username: string) {
return this.dbRead.prisma.user.findFirst({
where: {
@@ -91,6 +102,21 @@ export class UsersRepository {
});
}
async findManagedUsersByOAuthClientId(oauthClientId: string, cursor: number, limit: number) {
return this.dbRead.prisma.user.findMany({
where: {
platformOAuthClients: {
some: {
id: oauthClientId,
},
},
isPlatformManaged: true,
},
take: limit,
skip: cursor,
});
}
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
this.formatInput(updateData);
+230 -5
View File
@@ -71,6 +71,31 @@
}
},
"/api/v2/oauth-clients/{clientId}/users": {
"get": {
"operationId": "OAuthClientUsersController_getManagedUsers",
"parameters": [
{
"name": "clientId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
},
"post": {
"operationId": "OAuthClientUsersController_createUser",
"parameters": [
@@ -552,7 +577,7 @@
"content": {
"application/json": {
"schema": {
"type": "object"
"$ref": "#/components/schemas/GetEventTypesOutput"
}
}
}
@@ -582,7 +607,7 @@
"content": {
"application/json": {
"schema": {
"type": "object"
"$ref": "#/components/schemas/GetEventTypeOutput"
}
}
}
@@ -620,7 +645,7 @@
"content": {
"application/json": {
"schema": {
"type": "object"
"$ref": "#/components/schemas/UpdateEventTypeOutput"
}
}
}
@@ -648,7 +673,7 @@
"content": {
"application/json": {
"schema": {
"type": "object"
"$ref": "#/components/schemas/DeleteEventTypeOutput"
}
}
}
@@ -678,7 +703,7 @@
"content": {
"application/json": {
"schema": {
"type": "object"
"$ref": "#/components/schemas/GetEventTypesPublicOutput"
}
}
}
@@ -1720,6 +1745,139 @@
"data"
]
},
"Data": {
"type": "object",
"properties": {
"eventType": {
"$ref": "#/components/schemas/EventTypeOutput"
}
},
"required": [
"eventType"
]
},
"GetEventTypeOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/Data"
}
},
"required": [
"status",
"data"
]
},
"EventTypeGroup": {
"type": "object",
"properties": {
"eventTypes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EventTypeOutput"
}
}
},
"required": [
"eventTypes"
]
},
"GetEventTypesData": {
"type": "object",
"properties": {
"eventTypeGroups": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EventTypeGroup"
}
}
},
"required": [
"eventTypeGroups"
]
},
"GetEventTypesOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/GetEventTypesData"
}
},
"required": [
"status",
"data"
]
},
"PublicEventType": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 1
},
"length": {
"type": "number",
"example": 60
},
"slug": {
"type": "string",
"example": "cooking-class"
},
"title": {
"type": "string",
"example": "Learn the secrets of masterchief!"
},
"description": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"length",
"slug",
"title"
]
},
"GetEventTypesPublicOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PublicEventType"
}
}
},
"required": [
"status",
"data"
]
},
"UpdateEventTypeInput": {
"type": "object",
"properties": {
@@ -1747,6 +1905,73 @@
}
}
},
"UpdateEventTypeOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/EventTypeOutput"
}
},
"required": [
"status",
"data"
]
},
"DeleteData": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 1
},
"length": {
"type": "number",
"example": 60
},
"slug": {
"type": "string",
"example": "cooking-class"
},
"title": {
"type": "string",
"example": "Learn the secrets of masterchief!"
}
},
"required": [
"id",
"length",
"slug",
"title"
]
},
"DeleteEventTypeOutput": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/DeleteData"
}
},
"required": [
"status",
"data"
]
},
"CreateAvailabilityInput": {
"type": "object",
"properties": {
@@ -1,16 +1,16 @@
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
@Injectable()
export class NextAuthMockStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
export class NextAuthMockStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
super();
}
async authenticate() {
try {
const user = await this.userRepository.findByEmail(this.email);
const user = await this.userRepository.findByEmailWithProfile(this.email);
if (!user) {
throw new Error("User with the provided email not found");
}
+2 -1
View File
@@ -9,7 +9,8 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
LOG_LEVEL: "trace",
REDIS_URL: "redis://localhost:9199",
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
process.env = {
...env,
...process.env,
@@ -1,11 +0,0 @@
import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("set_a_password"),
(t) => t("set_a_password_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { X } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
export default function AddToHomescreen() {
const { t } = useLocale();
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<X className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
</button>
</div>
</div>
+2 -3
View File
@@ -9,8 +9,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText, Avatar } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
type ShouldHighlight =
| {
@@ -105,7 +104,7 @@ export default function AppListCard(props: AppListCardProps) {
<ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
+7 -8
View File
@@ -3,40 +3,39 @@
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, ButtonGroup } from "@calcom/ui";
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
import { Button, ButtonGroup, Icon } from "@calcom/ui";
export default function EnterprisePage() {
const { t } = useLocale();
const features = [
{
icon: <Globe className="h-5 w-5 text-red-500" />,
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
title: t("branded_subdomain"),
description: t("branded_subdomain_description"),
},
{
icon: <BarChart className="h-5 w-5 text-blue-500" />,
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
title: t("org_insights"),
description: t("org_insights_description"),
},
{
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
title: t("extensive_whitelabeling"),
description: t("extensive_whitelabeling_description"),
},
{
icon: <Users className="h-5 w-5 text-orange-500" />,
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
title: t("unlimited_teams"),
description: t("unlimited_teams_description"),
},
{
icon: <CreditCard className="h-5 w-5 text-green-500" />,
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
title: t("unified_billing"),
description: t("unified_billing_description"),
},
{
icon: <Lock className="h-5 w-5 text-purple-500" />,
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
title: t("advanced_managed_events"),
description: t("advanced_managed_events_description"),
},
@@ -6,12 +6,11 @@ import { trpc } from "@calcom/trpc/react";
import {
Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -42,7 +41,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
<Button StartIcon="plus" color="secondary" {...(isPending && { loading: isPending })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
@@ -50,7 +49,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
{options.map((data) => (
<DropdownMenuItem key={data.slug} className="focus:outline-none">
{data.slug === "add-new" ? (
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
<DropdownItem StartIcon="plus" color="minimal" href="/apps/categories/calendar">
{t("install_new_calendar_app")}
</DropdownItem>
) : (
+6 -7
View File
@@ -11,17 +11,16 @@ import type { AppCategories } from "@calcom/prisma/enums";
import { trpc, type RouterOutputs } from "@calcom/trpc";
import type { App } from "@calcom/types/App";
import {
Alert,
Button,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
List,
showToast,
Button,
DropdownMenuItem,
Alert,
} from "@calcom/ui";
import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
import AppListCard from "@components/AppListCard";
@@ -82,7 +81,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
<div className="flex justify-end">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
<Button StartIcon="ellipsis" variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
@@ -90,7 +89,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
<DropdownItem
type="button"
color="secondary"
StartIcon={Video}
StartIcon="video"
onClick={() => {
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
if (locationType?.linkType === "static") {
@@ -214,7 +213,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
color="destructive"
onClick={() => handleDisconnect(credentialId, teamId)}
disabled={isGlobal}
StartIcon={Trash}>
StartIcon="trash">
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
+9 -10
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import type { IframeHTMLAttributes } from "react";
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
@@ -10,8 +10,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
import { Badge, Button, Icon, SkeletonButton, SkeletonText, showToast } from "@calcom/ui";
import { InstallAppButtonChild } from "./InstallAppButtonChild";
@@ -196,7 +195,7 @@ export const AppPage = ({
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={Check} color="secondary" disabled>
<Button StartIcon="check" color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
@@ -323,7 +322,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
@@ -335,7 +334,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
@@ -347,7 +346,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={`mailto:${email}`}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email}
</a>
@@ -360,7 +359,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
@@ -372,7 +371,7 @@ export const AppPage = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
@@ -383,7 +382,7 @@ export const AppPage = ({
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Flag className="inline h-3 w-3" /> {t("report_app")}
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
@@ -11,13 +11,12 @@ import {
Alert,
Button,
EmptyScreen,
List,
AppSkeletonLoader as SkeletonLoader,
ShellSubHeading,
Label,
List,
ShellSubHeading,
AppSkeletonLoader as SkeletonLoader,
showToast,
} from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -267,7 +266,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</>
) : (
<EmptyScreen
Icon={Calendar}
Icon="calendar"
headline={t("no_category_apps", {
category: t("calendar").toLowerCase(),
})}
@@ -122,14 +122,13 @@ export const InstallAppButtonChild = ({
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={isInstalled}
StartIcon={(props: { className?: string }) => (
CustomStartIcon={
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
size="sm"
{...props}
/>
)}
}
onClick={() => {
mutation.mutate(
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
@@ -6,7 +6,6 @@ import React from "react";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = {
children: React.ReactNode;
@@ -27,7 +26,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full">
{emptyStore ? (
<EmptyScreen
Icon={AlertCircle}
Icon="circle-alert"
headline={isAdmin ? t("no_apps") : t("no_apps_configured")}
description={isAdmin ? t("enable_in_settings") : t("please_contact_admin")}
buttonText={isAdmin ? t("apps_settings") : ""}
+12
View File
@@ -0,0 +1,12 @@
import type { TurnstileProps } from "react-turnstile";
import Turnstile from "react-turnstile";
import { CLOUDFLARE_SITE_ID } from "@calcom/lib/constants";
type Props = Omit<TurnstileProps, "sitekey">;
export default function TurnstileWidget(props: Props) {
if (!CLOUDFLARE_SITE_ID || process.env.NEXT_PUBLIC_IS_E2E) return null;
return <Turnstile {...props} sitekey={CLOUDFLARE_SITE_ID} theme="auto" />;
}
@@ -1,7 +1,4 @@
import React from "react";
import { Button, SkeletonText } from "@calcom/ui";
import { MoreHorizontal } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
@@ -31,7 +28,7 @@ function SkeletonItem() {
type="button"
variant="icon"
color="secondary"
StartIcon={MoreHorizontal}
StartIcon="ellipsis"
disabled
/>
</div>
+11 -10
View File
@@ -29,13 +29,13 @@ import {
DialogClose,
DialogContent,
DialogFooter,
Icon,
MeetingTimeInTimezones,
showToast,
TableActions,
TextAreaField,
Tooltip,
} from "@calcom/ui";
import { Ban, Check, Clock, CreditCard, MapPin, RefreshCcw, Send, X } from "@calcom/ui/components/icon";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -138,7 +138,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: Ban,
icon: "ban",
disabled: mutation.isPending,
},
// For bookings with payment, only confirm if the booking is paid for
@@ -152,7 +152,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
bookingConfirm(true);
},
icon: Check,
icon: "check" as const,
disabled: mutation.isPending,
},
]
@@ -169,7 +169,7 @@ function BookingListItem(booking: BookingItemProps) {
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: X,
icon: "x" as const,
},
{
id: "edit_booking",
@@ -177,7 +177,7 @@ function BookingListItem(booking: BookingItemProps) {
actions: [
{
id: "reschedule",
icon: Clock,
icon: "clock" as const,
label: t("reschedule_booking"),
href: `${bookerUrl}/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
@@ -185,7 +185,7 @@ function BookingListItem(booking: BookingItemProps) {
},
{
id: "reschedule_request",
icon: Send,
icon: "send" as const,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"),
onClick: () => {
@@ -198,7 +198,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: MapPin,
icon: "map-pin" as const,
},
],
},
@@ -212,7 +212,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setChargeCardDialogIsOpen(true);
},
icon: CreditCard,
icon: "credit-card" as const,
},
];
@@ -226,7 +226,7 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => {
return (
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
<Badge startIcon="send" size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")}
</Badge>
);
@@ -599,7 +599,8 @@ const RecurringBookingsTooltip = ({
);
})}>
<div className="text-default">
<RefreshCcw
<Icon
name="refresh-ccw"
strokeWidth="3"
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
/>
@@ -5,8 +5,7 @@ import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, TextArea } from "@calcom/ui";
import { X } from "@calcom/ui/components/icon";
import { Button, Icon, TextArea } from "@calcom/ui";
type Props = {
booking: {
@@ -57,7 +56,7 @@ export default function CancelBooking(props: Props) {
{error && (
<div className="mt-8">
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<X className="h-6 w-6 text-red-600" />
<Icon name="x" className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -1,5 +1,5 @@
import { useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -10,9 +10,9 @@ import {
DialogContent,
DialogFooter,
DialogHeader,
Icon,
showToast,
} from "@calcom/ui";
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -48,7 +48,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className=" bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full">
<CreditCard className="m-auto h-6 w-6" />
<Icon name="credit-card" className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
@@ -56,7 +56,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
{chargeError && (
<div className="mt-4 flex text-red-500">
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<Icon name="triangle-alert" className="mr-2 h-5 w-5 " aria-hidden="true" />
<p className="text-sm">{t("error_charging_card")}</p>
</div>
)}
@@ -18,9 +18,7 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { MapPin } from "@calcom/ui/components/icon";
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
@@ -237,7 +235,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="bg-subtle mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<MapPin className="text-emphasis h-6 w-6" />
<Icon name="map-pin" className="text-emphasis h-6 w-6" />
</div>
<div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
@@ -10,10 +10,10 @@ import {
DialogContent,
DialogFooter,
DialogHeader,
Icon,
showToast,
TextArea,
} from "@calcom/ui";
import { Clock } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -44,7 +44,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Clock className="m-auto h-6 w-6" />
<Icon name="clock" className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
@@ -21,7 +21,6 @@ import {
PhoneInput,
showToast,
} from "@calcom/ui";
import { Sparkles } from "@calcom/ui/components/icon";
type AIEventControllerProps = {
eventType: EventTypeSetup;
@@ -44,7 +43,7 @@ export default function AIEventController({ eventType, isTeamEvent }: AIEventCon
{!isOrg || !isTeamEvent ? (
<EmptyScreen
headline={t("Cal.ai")}
Icon={Sparkles}
Icon="sparkles"
description={t("upgrade_to_cal_ai_phone_number_description")}
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
/>
@@ -127,8 +126,15 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
const values = formMethods.getValues("aiPhoneCallConfig");
const data = await AIPhoneSettingSchema.parseAsync({
...values,
generalPrompt: values.generalPrompt,
beginMessage: values.beginMessage,
enabled: values.enabled,
guestName: values.guestName,
guestEmail: values.guestEmail.trim().length ? values.guestEmail : undefined,
guestCompany: values.guestCompany.trim().length ? values.guestCompany : undefined,
eventTypeId: eventType.id,
numberToCall: values.numberToCall,
yourPhoneNumber: values.yourPhoneNumber,
calApiKey,
});
@@ -191,12 +197,28 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
<TextField
type="text"
hint="Variable: {name}"
hint="Variable: {{name}}"
label={t("guest_name")}
placeholder="Jane Doe"
{...formMethods.register("aiPhoneCallConfig.guestName")}
/>
<TextField
type="text"
hint="Variable: {{email}}"
label={t("guest_email")}
placeholder="jane@acme.com"
{...formMethods.register("aiPhoneCallConfig.guestEmail")}
/>
<TextField
type="text"
hint="Variable: {{company}}"
label={t("guest_company")}
placeholder="Acme"
{...formMethods.register("aiPhoneCallConfig.guestCompany")}
/>
<TextField
type="text"
hint="For eg:- cal_live_0123.."
@@ -1,4 +1,3 @@
import { InfoIcon } from "lucide-react";
import dynamic from "next/dynamic";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
@@ -30,15 +29,15 @@ import {
Button,
Badge,
CheckboxField,
Icon,
Label,
SelectField,
SettingsToggle,
showToast,
Switch,
TextField,
Tooltip,
showToast,
} from "@calcom/ui";
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -180,7 +179,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
aria-label="edit custom name"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}>
<Edit className="h-4 w-4" />
<Icon name="pencil" className="h-4 w-4" />
</Button>
}
/>
@@ -194,7 +193,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
label={
<>
{t("display_add_to_calendar_organizer")}
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
<Icon
name="info"
className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md"
/>
</>
}
checked={useEventTypeDestinationCalendarEmail}
@@ -348,7 +350,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
target="_blank"
rel="noreferrer"
href="https://cal.com/docs/core-features/event-types/single-use-private-links">
<Info className="ml-1.5 h-4 w-4 cursor-pointer" />
<Icon name="info" className="ml-1.5 h-4 w-4 cursor-pointer" />
</a>
}
{...shouldLockDisableProps("hashedLink")}
@@ -389,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
showToast(t("enabled_after_update_description"), "warning");
}
}}>
<Copy className="h-4 w-4" />
<Icon name="copy" className="h-4 w-4" />
</Button>
</Tooltip>
}
@@ -11,8 +11,7 @@ import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hook
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, Alert } from "@calcom/ui";
import { Grid, Lock } from "@calcom/ui/components/icon";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
@@ -161,12 +160,12 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
)}
{!isPending && !installedApps?.length ? (
<EmptyScreen
Icon={Grid}
Icon="grid-3x3"
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
appsDisableProps.disabled ? (
<Button StartIcon={Lock} color="secondary" disabled>
<Button StartIcon="lock" color="secondary" disabled>
{t("locked_by_team_admin")}
</Button>
) : (
@@ -13,8 +13,7 @@ import { weekdayNames } from "@calcom/lib/weekday";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
@@ -130,7 +129,7 @@ const EventTypeScheduleDetails = memo(
</div>
<div className="bg-muted border-subtle flex flex-col justify-center gap-2 rounded-b-md border p-6 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
<Icon name="globe" className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
@@ -138,7 +137,7 @@ const EventTypeScheduleDetails = memo(
href={`/availability/${schedule.id}`}
disabled={isPending}
color="minimal"
EndIcon={ExternalLink}
EndIcon="external-link"
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
@@ -18,7 +18,6 @@ import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/interval
import type { PeriodType } from "@calcom/prisma/enums";
import type { IntervalLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
import { Plus, Trash2 } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
@@ -598,7 +597,7 @@ const IntervalLimitItem = ({
{hasDeleteButton && !disabled && (
<Button
variant="icon"
StartIcon={Trash2}
StartIcon="trash-2"
color="destructive"
className="border-none"
onClick={() => onDelete(limitKey)}
@@ -708,7 +707,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
);
})}
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
<Button color="minimal" StartIcon="plus" onClick={addLimit}>
{t("add_limit")}
</Button>
)}
@@ -23,6 +23,7 @@ import {
SettingsToggle,
Skeleton,
TextField,
Icon,
Editor,
SkeletonContainer,
SkeletonText,
@@ -31,7 +32,6 @@ import {
Button,
showToast,
} from "@calcom/ui";
import { Plus, X, Check, CornerDownRight } from "@calcom/ui/components/icon";
import CheckboxField from "@components/ui/form/CheckboxField";
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
@@ -313,7 +313,7 @@ export const EventSetupTab = (
onClick={() => remove(index)}
aria-label={t("remove")}>
<div className="h-4 w-4">
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
<Icon name="x" className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
</div>
</button>
)}
@@ -324,7 +324,7 @@ export const EventSetupTab = (
<div className="w-full">
<div className="flex gap-2">
<div className="flex items-center justify-center">
<CornerDownRight className="h-4 w-4" />
<Icon name="corner-down-right" className="h-4 w-4" />
</div>
<LocationInput
defaultValue={
@@ -414,7 +414,7 @@ export const EventSetupTab = (
) && (
<div className="text-default flex items-center text-sm">
<div className="mr-1.5 h-3 w-3">
<Check className="h-3 w-3" />
<Icon name="check" className="h-3 w-3" />
</div>
<Trans i18nKey="event_type_requres_google_cal">
<p>
@@ -442,7 +442,7 @@ export const EventSetupTab = (
<li>
<Button
data-testid="add-location"
StartIcon={Plus}
StartIcon="plus"
color="minimal"
onClick={() => setShowEmptyLocationSelect(true)}>
{t("add_location")}
@@ -1,4 +1,3 @@
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/navigation";
@@ -15,7 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import type { DialogProps } from "@calcom/ui";
import type { DialogProps, VerticalTabItemProps } from "@calcom/ui";
import {
Button,
ButtonGroup,
@@ -29,6 +28,7 @@ import {
DropdownMenuTrigger,
HorizontalTabs,
Label,
Icon,
showToast,
Skeleton,
Switch,
@@ -36,23 +36,6 @@ import {
VerticalDivider,
VerticalTabs,
} from "@calcom/ui";
import {
Link as LinkIcon,
Calendar,
Clock,
Sliders,
Repeat,
Grid,
Zap,
Sparkles,
Users,
ExternalLink,
Code,
Trash,
PhoneCall,
MoreHorizontal,
Loader,
} from "@calcom/ui/components/icon";
type Props = {
children: React.ReactNode;
@@ -97,41 +80,41 @@ function getNavigation({
{
name: "event_setup_tab_title",
href: `/event-types/${id}?tabName=setup`,
icon: LinkIcon,
icon: "link",
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${id}?tabName=limits`,
icon: Clock,
icon: "clock",
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${id}?tabName=advanced`,
icon: Sliders,
icon: "sliders-vertical",
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${id}?tabName=recurring`,
icon: Repeat,
icon: "repeat",
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${id}?tabName=apps`,
icon: Grid,
icon: "grid-3x3",
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${id}?tabName=workflows`,
icon: Zap,
icon: "zap",
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
] satisfies VerticalTabItemProps[];
}
function DeleteDialog({
@@ -228,7 +211,7 @@ function EventTypeSingleLayout({
const watchChildrenCount = formMethods.watch("children").length;
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
const navigation: VerticalTabItemProps[] = getNavigation({
t,
length,
multipleDuration,
@@ -242,7 +225,7 @@ function EventTypeSingleLayout({
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: Calendar,
icon: "calendar",
info:
isManagedEventType || isChildrenManagedEventType
? formMethods.getValues("schedule") === null
@@ -261,7 +244,7 @@ function EventTypeSingleLayout({
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: Users,
icon: "users",
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
@@ -273,14 +256,14 @@ function EventTypeSingleLayout({
navigation.push({
name: "instant_tab_title",
href: `/event-types/${eventType.id}?tabName=instant`,
icon: PhoneCall,
icon: "phone-call",
info: `instant_event_tab_description`,
});
}
navigation.push({
name: "webhooks",
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: TbWebhook,
icon: "webhook",
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
@@ -289,7 +272,7 @@ function EventTypeSingleLayout({
navigation.push({
name: "Cal.ai",
href: `/event-types/${eventType.id}?tabName=ai`,
icon: Sparkles,
icon: "sparkles",
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
});
}
@@ -378,14 +361,14 @@ function EventTypeSingleLayout({
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={ExternalLink}
StartIcon="external-link"
/>
</Tooltip>
<Button
color="secondary"
variant="icon"
StartIcon={LinkIcon}
StartIcon="link"
tooltip={t("copy_link")}
tooltipSide="bottom"
tooltipOffset={4}
@@ -396,7 +379,7 @@ function EventTypeSingleLayout({
/>
<EventTypeEmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
StartIcon="code"
color="secondary"
variant="icon"
namespace=""
@@ -411,7 +394,7 @@ function EventTypeSingleLayout({
<Button
color="destructive"
variant="icon"
StartIcon={Trash}
StartIcon="trash"
tooltip={t("delete")}
tooltipSide="bottom"
tooltipOffset={4}
@@ -425,14 +408,14 @@ function EventTypeSingleLayout({
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
target="_blank"
type="button"
StartIcon={ExternalLink}
StartIcon="external-link"
href={permalink}
rel="noreferrer">
{t("preview")}
@@ -441,7 +424,7 @@ function EventTypeSingleLayout({
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
StartIcon={LinkIcon}
StartIcon="link"
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
@@ -453,7 +436,7 @@ function EventTypeSingleLayout({
<DropdownItem
type="button"
color="destructive"
StartIcon={Trash}
StartIcon="trash"
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
@@ -489,7 +472,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<Suspense fallback={<Loader />}>
<Suspense fallback={<Icon name="loader" />}>
<div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block">
<VerticalTabs
@@ -1,5 +1,4 @@
import type { Webhook } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
@@ -16,7 +15,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { Plus, Lock } from "@calcom/ui/components/icon";
export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
@@ -92,7 +90,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={Plus}
StartIcon="plus"
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
@@ -160,12 +158,12 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
</>
) : (
<EmptyScreen
Icon={TbWebhook}
Icon="webhook"
headline={t("create_your_first_webhook")}
description={t("first_event_type_webhook_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
<Button StartIcon="lock" color="secondary" disabled>
{t("locked_by_team_admin")}
</Button>
) : (
@@ -16,7 +16,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
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;
@@ -48,7 +47,7 @@ export default function InstantEventController({
{!isOrg || !isTeamEvent ? (
<EmptyScreen
headline={t("instant_tab_title")}
Icon={PhoneCall}
Icon="phone-call"
description={t("uprade_to_create_instant_bookings")}
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
/>
@@ -171,7 +170,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={Plus}
StartIcon="plus"
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
@@ -218,12 +217,12 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
{t("warning_payment_instant_meeting_event")}
</p>
<EmptyScreen
Icon={TbWebhook}
Icon="webhook"
headline={t("create_your_first_webhook")}
description={t("create_instant_meeting_webhook_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
<Button StartIcon="lock" color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
@@ -1,5 +1,5 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { Clock, User } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
function SkeletonLoader() {
return (
@@ -32,11 +32,11 @@ function SkeletonItem() {
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<Clock className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
<Icon name="clock" className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<User className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
<Icon name="user" className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
</ul>
@@ -1,8 +1,7 @@
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { List } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Icon, List } from "@calcom/ui";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
@@ -88,7 +87,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
onClick={() => nextStep()}
disabled={disabledNextButton}>
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
@@ -1,8 +1,7 @@
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { List } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Icon, List } from "@calcom/ui";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
@@ -59,7 +58,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
disabled={!hasAnyInstalledVideoApps}
onClick={() => nextStep()}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
@@ -6,8 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Form } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Button, Form, Icon } from "@calcom/ui";
interface ISetupAvailabilityProps {
nextStep: () => void;
@@ -78,7 +77,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
loading={availabilityForm.formState.isSubmitting}
disabled={availabilityForm.formState.isSubmitting}>
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
{t("next_step_text")} <Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" />
</Button>
</div>
</Form>
@@ -10,7 +10,6 @@ import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
type FormData = {
bio: string;
@@ -147,7 +146,7 @@ const UserProfile = () => {
</fieldset>
<Button
loading={mutation.isPending}
EndIcon={ArrowRight}
EndIcon="arrow-right"
type="submit"
className="mt-8 w-full items-center justify-center">
{t("finish")}
@@ -9,8 +9,7 @@ import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect, Input } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Button, TimezoneSelect, Icon, Input } from "@calcom/ui";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
@@ -116,7 +115,7 @@ const UserSettings = (props: IUserSettingsProps) => {
loading={mutation.isPending}
disabled={mutation.isPending}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
</form>
);
@@ -1,10 +1,10 @@
import { Shield } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<Shield className="text-inverted h-6 w-6" />
<Icon name="shield" className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -14,7 +14,6 @@ import {
Button,
InputError,
} from "@calcom/ui";
import { MoreHorizontal, Flag, Trash, Send } from "@calcom/ui/components/icon";
type CustomEmailTextFieldProps = {
formMethods: UseFormReturn<FormValues>;
@@ -71,7 +70,7 @@ const CustomEmailTextField = ({
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
StartIcon={MoreHorizontal}
StartIcon="ellipsis"
variant="icon"
size="sm"
color="secondary"
@@ -82,7 +81,7 @@ const CustomEmailTextField = ({
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
StartIcon={Flag}
StartIcon="flag"
color="secondary"
className="disabled:opacity-40"
onClick={handleChangePrimary}
@@ -94,7 +93,7 @@ const CustomEmailTextField = ({
{!emailVerified && (
<DropdownMenuItem>
<DropdownItem
StartIcon={Send}
StartIcon="send"
color="secondary"
className="disabled:opacity-40"
onClick={handleVerifyEmail}
@@ -106,7 +105,7 @@ const CustomEmailTextField = ({
)}
<DropdownMenuItem>
<DropdownItem
StartIcon={Trash}
StartIcon="trash"
color="destructive"
className="disabled:opacity-40"
onClick={handleItemDelete}
@@ -1,10 +1,9 @@
import { Asterisk, Clipboard } from "lucide-react";
import React from "react";
import { classNames } from "@calcom/lib";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants";
import type { Avatar } from "@calcom/prisma/client";
import { Button, showToast } from "@calcom/ui";
import { Button, Icon, showToast } from "@calcom/ui";
import { hasPermission } from "../../../../../../../packages/platform/utils/permissions";
@@ -75,7 +74,8 @@ export const OAuthClientCard = ({
<div className="flex flex-row items-center gap-2">
<div className="font-semibold">Client Id:</div>
<div>{id}</div>
<Clipboard
<Icon
name="clipboard"
type="button"
className="h-4 w-4 cursor-pointer"
onClick={() => {
@@ -89,9 +89,10 @@ export const OAuthClientCard = ({
<div className="font-semibold">Client Secret:</div>
<div className="flex items-center justify-center rounded-md">
{[...new Array(20)].map((_, index) => (
<Asterisk key={`${index}asterisk`} className="h-2 w-2" />
<Icon name="asterisk" key={`${index}asterisk`} className="h-2 w-2" />
))}
<Clipboard
<Icon
name="clipboard"
type="button"
className="ml-2 h-4 w-4 cursor-pointer"
onClick={() => {
@@ -7,7 +7,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
import { showToast } from "@calcom/ui";
import { Meta, Button, TextField, Label } from "@calcom/ui";
import { Plus, Trash } from "@calcom/ui/components/icon";
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
@@ -156,7 +155,7 @@ export const OAuthClientForm: FC = () => {
type="button"
color="minimal"
variant="icon"
StartIcon={Plus}
StartIcon="plus"
className="text-default mx-2 mb-2"
onClick={() => {
append({ uri: "" });
@@ -168,7 +167,7 @@ export const OAuthClientForm: FC = () => {
type="button"
color="destructive"
variant="icon"
StartIcon={Trash}
StartIcon="trash"
className="text-default mx-2 mb-2"
onClick={() => {
remove(index);
@@ -191,7 +190,7 @@ export const OAuthClientForm: FC = () => {
<Avatar
alt=""
imageSrc={value}
fallback={<Plus className="text-subtle h-4 w-4" />}
fallback={<Icon name="plus" className="text-subtle h-4 w-4" />}
size="sm"
/>
<div className="ms-4">
+1 -2
View File
@@ -9,7 +9,6 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { UserCheck } from "@calcom/ui/components/icon";
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
@@ -24,7 +23,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
props.onSuccess();
}}>
<EmptyScreen
Icon={UserCheck}
Icon="user-check"
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
@@ -11,7 +11,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
type EnterpriseLicenseFormValues = {
licenseKey: string;
@@ -92,7 +92,7 @@ const EnterpriseLicense = (
<div>
<Button
className="w-full justify-center text-lg"
EndIcon={ExternalLink}
EndIcon="external-link"
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
@@ -122,9 +122,9 @@ const EnterpriseLicense = (
)}
addOnSuffix={
checkLicenseLoading ? (
<Loader className="h-5 w-5 animate-spin" />
<Icon name="loader" className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<Check className="h-5 w-5 text-green-700" />
<Icon name="check" className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
+5 -2
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Check } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
const StepDone = (props: {
currentStep: number;
@@ -24,7 +24,10 @@ const StepDone = (props: {
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
<Icon
name="check"
className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10"
/>
</div>
<div className="max-w-[420px] text-center">
<h2 className="mb-1 mt-6 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
+3 -5
View File
@@ -1,11 +1,9 @@
import React from "react";
import type { LucideIcon } from "@calcom/ui/components/icon";
import type { SVGComponent } from "@lib/types/SVGComponent";
import { Icon } from "@calcom/ui";
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Icon: SVGComponent | LucideIcon;
Icon: React.ComponentProps<typeof Icon>["name"];
}
export default function LinkIconButton(props: LinkIconButtonProps) {
@@ -15,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
type="button"
{...props}
className="text-md hover:bg-emphasis hover:text-emphasis text-default flex items-center rounded-sm px-2 py-1 text-sm font-medium">
<props.Icon className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
<Icon name={props.Icon} className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
{props.children}
</button>
</div>
@@ -16,7 +16,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Input, Label } from "@calcom/ui";
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE",
@@ -239,9 +239,13 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
isInputUsernamePremium ? "text-transparent" : "",
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
{isInputUsernamePremium ? (
<Icon name="star" className="mt-[2px] h-4 w-4 fill-orange-400" />
) : (
<></>
)}
{!isInputUsernamePremium && usernameIsAvailable ? (
<Check className="mt-[2px] h-4 w-4" />
<Icon name="check" className="mt-[2px] h-4 w-4" />
) : (
<></>
)}
@@ -260,7 +264,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<Dialog open={openDialogSaveUsername}>
<DialogContent
Icon={Edit2}
Icon="pencil"
title={t("confirm_username_change_dialog_title")}
description={
<>
@@ -297,7 +301,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
data-testid="go-to-billing"
href={paymentLink}>
<>
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
{t("go_to_stripe_billing")} <Icon name="external-link" className="ml-1 h-4 w-4" />
</>
</Button>
)}
@@ -11,7 +11,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, TextField, DialogFooter, Tooltip } from "@calcom/ui";
import { Check, Edit2 } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
interface ICustomUsernameProps {
currentUsername: string | undefined;
@@ -137,7 +137,11 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex h-7 flex-row">
<span className={classNames("bg-default mx-0 p-3")}>
{usernameIsAvailable ? <Check className="relative bottom-[6px] h-4 w-4" /> : <></>}
{usernameIsAvailable ? (
<Icon name="check" className="relative bottom-[6px] h-4 w-4" />
) : (
<></>
)}
</span>
</div>
)}
@@ -154,7 +158,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
</div>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
<DialogContent type="confirmation" Icon="pencil" title={t("confirm_username_change_dialog_title")}>
<div className="flex flex-row">
<div className="mb-4 w-full pt-1">
<div className="bg-subtle flex w-full flex-wrap justify-between gap-6 rounded-sm px-4 py-3 text-sm">
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui";
import { X } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
import Select from "@components/ui/form/Select";
@@ -43,7 +43,8 @@ export const CheckedSelect = ({
alt={option.label}
/>
{option.label}
<X
<Icon
name="x"
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
/>
+2 -2
View File
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { Calendar } from "@calcom/ui/components/icon";
import { Icon } from "@calcom/ui";
import classNames from "@lib/classNames";
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
className
)}
clearIcon={null}
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
calendarIcon={<Icon name="calendar" className="text-subtle h-5 w-5" />}
value={date}
minDate={minDate}
disabled={disabled}
@@ -8,7 +8,9 @@ import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomain
import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/enums";
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -57,6 +59,19 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
return notFound;
}
if (!org) {
const redirect = await getTemporaryOrgRedirect({
slugs: [username],
redirectType: RedirectType.User,
eventTypeSlug: slug,
currentQuery: context.query,
});
if (redirect) {
return redirect;
}
}
const [user] = await UserRepository.findUsersByUsername({
usernameList: [username],
orgSlug: org,
+12 -12
View File
@@ -63,7 +63,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/30min",
destination: "https://calcom.cal.com/30min?orgRedirection=true",
},
});
});
@@ -83,7 +83,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/30min?abc=1",
destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true",
},
});
});
@@ -103,7 +103,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com?abc=1",
destination: "https://calcom.cal.com?abc=1&orgRedirection=true",
},
});
});
@@ -127,7 +127,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com?abc=1",
destination: "https://calcom.cal.com?abc=1&orgRedirection=true",
},
});
});
@@ -151,7 +151,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/30min?abc=1",
destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true",
},
});
});
@@ -173,7 +173,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/30min",
destination: "https://calcom.cal.com/30min?orgRedirection=true",
},
});
});
@@ -200,7 +200,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true",
},
});
});
@@ -227,7 +227,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true",
},
});
@@ -241,7 +241,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect1).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/second-in-org1+first-in-org1",
destination: "https://calcom.cal.com/second-in-org1+first-in-org1?orgRedirection=true",
},
});
});
@@ -270,7 +270,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1",
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1&orgRedirection=true",
},
});
});
@@ -297,7 +297,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min",
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?orgRedirection=true",
},
});
});
@@ -326,7 +326,7 @@ describe("getTemporaryOrgRedirect", () => {
expect(redirect).toEqual({
redirect: {
permanent: false,
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1",
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1&orgRedirection=true",
},
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ export const getTemporaryOrgRedirect = async ({
// Use the first redirect origin as the new origin as we aren't supposed to handle different org usernames in a group
const newOrigin = new URL(redirects[0].toUrl).origin;
const query = currentQueryString ? `?${currentQueryString}` : "";
const query = currentQueryString ? `?${currentQueryString}&orgRedirection=true` : "?orgRedirection=true";
// Use the same order as in input slugs - It is important from Dynamic Group perspective as the first user's settings are used for various things
const newSlugs = slugs.map((slug) => {
const redirect = redirects.find((redirect) => redirect.from === slug);
@@ -19,7 +19,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
},
});
if (team) {
return GSSTeamPage({ ...ctx, query: { slug: ctx.query.user } });
return GSSTeamPage({
...ctx,
query: { slug: ctx.query.user, orgRedirection: ctx.query.orgRedirection },
});
}
return GSSUserPage({ ...ctx, query: { user: ctx.query.user, redirect: ctx.query.redirect } });
return GSSUserPage({
...ctx,
query: {
user: ctx.query.user,
redirect: ctx.query.redirect,
orgRedirection: ctx.query.orgRedirection,
},
});
};
+4 -2
View File
@@ -24,8 +24,10 @@ const querySchema = z.object({
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const emailVerificationEnabled = await getFeatureFlag(prisma, "email-verification");
await ssrInit(ctx);
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
const ssr = await ssrInit(ctx);
const token = z.string().optional().parse(ctx.query.token);
const redirectUrlData = z
.string()
@@ -42,8 +44,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
redirectUrl,
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
trpcState: ssr.dehydrate(),
prepopulateFormValues: undefined,
emailVerificationEnabled,
};
// username + email prepopulated from query params
@@ -72,6 +72,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventSlug: meetingSlug,
isTeamEvent: true,
org,
fromRedirectOfNonOrgLink: context.query.orgRedirection === "true",
});
if (!eventData) {

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