Files
calendar/packages/features/embed/Embed.tsx
T
+24 0a15483c46 feat: platform atoms + api v2 (#14106)
* add connect to cal atom

* add connect to cal to atoms package index

* shadcn setup

* update packages

* button component from shadcn

* dumb button component

* replace button with dumb button component

* update commenst and change url param

* chore: setting up monorepo packages for platforms (#12572)

* chore: init platform folders strucutre and api proxy

* fixup! chore: init platform folders strucutre and api proxy

* chore: init database oAuth platform client schema

* fixup! chore: init database oAuth platform client schema

* fix config path after move to folder /v1

* feat: scaffold v2 API (#12554)

* init nest project

* add kysely as a separate package

* add ee folder with license

* prettier: format v2 api same as mono repo

* migrate over Eriks nestjs poc

* schema generation script

* ee license file

* nest poc eslint file

* remove prisma-kysely from v2 api

* prepare for mergin platform: remove prisma-kysely because incorrect checksum

* add prisma-kysely to packages/prisma

* move to apps/api/v2

* rename v2 package

* readme update

* Update apps/api/v2/env.example

* env typo

* env typo

---------

Co-authored-by: Erik <erik@erosemberg.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>

* chore: init platform packages (#12603)

* chore: init platform packages

* fixup! chore: init platform packages

* fixup! fixup! chore: init platform packages

* fix: type in package.json

* fixup! fixup! fixup! chore: init platform packages

* fixup! fixup! fixup! fixup! chore: init platform packages

* fixup! fixup! fixup! fixup! fixup! chore: init platform packages

* fixup! fixup! fixup! fixup! fixup! fixup! chore: init platform packages

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>

* chore: platform more permissions utils and tests (#12613)

* chore: more permisions utils and tests

* chore: tests

* feat: validate `CalProvider` api keys (#12672)

* fix: signup nit (#12585)

* Disable submit on empty form

* Fix submit

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* New Crowdin translations by Github Action

* fix: Signup options are not disabled (#12610)

* fix: Signup options are not disabled

* fixes/signup disabling suggested changes done

* chore: signup and login improvements

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>

* fix: typo in @calcom/emails readme (#12615)

* fix: handle reschedule request for dynamic meetings (#12275)

* chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561)

Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* chore: added cursor-pointer to img upload (#12624)

* added cursor-pointer to img upload

* nit

* feat: add clear filters option in bookings page (#12629)

* add clear filters option

* fix vscode settings.json

* use removeAllQueryParams()

* fix yarn lock

* remove toggleoption

* feat: display long durations in hours on booking (#12631)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* New Crowdin translations by Github Action

* fix: workaround for future app dir routes

* feat: Allow only first slot to be booked (#12636)

Co-authored-by: Morgan Vernay <morgan@cal.com>

* New Crowdin translations by Github Action

* feat: add matomo analytics app (#12646)

* chore: Sentry Wrapper with Performance and Error Tracing (#12642)

* add wrapper for sentry and update functions in 'getUserAvailability'. Update tracesSampleRate to 1.0

* Make Sentry Wrapper utilize parent transaction, if it exists.

* Update wrapper for functions to inherit parameters from the child function

* add comment of when to use the wrapper

* check for sentry before wrapping, if not call unwrapped function

* refactored wrapper to have async and sync separate functions that utilize helpers for common behaviour

* update type of args to unknown

* fixed types of returns from wrapped functions

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* validate api keys to set error and key states for api keys

* organize error messages into one place

* set error messages from errors file instead of hardcoding value

* fix incorrect constant name

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com>
Co-authored-by: Manpreet Singh <manpoffc@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com>
Co-authored-by: Mike Zhou <mikezhoudev@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com>
Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* fix: storybook atom import

* feat: oauth clients management (#12604)

* feat: oAuth create and update dtos

* feat: WIP oAuth repository

* feat: oAuth controller and module

* refactor: rename cal oAuth to simply oauth

* fix: controller update client

* add oAuth client module to endpoints module

* refactor: OAuthClientRepository

* refactor

* status codes for controller

* oauth create return

* fix: oAuth client only for v2 API

* remove repository as provider from its module

* fix: oAuth module, have repository in providers

* fix: make endpoints run

* comment out oauth prisma schema code: coming next

* refactor: remove api-key authguard (#12675)

* chore: Prisma exception filters (#12679)

* fix: get prisma schema from package @calcom/prisma (#12692)

* fix: get prisma schema from package @calcom/prisma

* fixup! fix: get prisma schema from package @calcom/prisma

* fixup! fixup! fix: get prisma schema from package @calcom/prisma

* fix: prettier config

* fixup! fix: prettier config

* chore: create authenticated user guard (#12677)

* chore: wip next-auth-guard for apiv2

* refactor: use NextAuthGuard on all OAuthClientController routes

* refactor: use passport to get next user

* exclude password from prisma read service

* Revert "exclude password from prisma read service"

This reverts commit 24b1f8ed59f6d99f0ecc589c691efb0ac1f2ac3b.

* fix: userRepo sanitize password

* refactor: function order in UserRepository

* implement user repository service

* refactor: strict null checks for TS

* refactor: fix strict null checks

* delete res locals type

* refactor: switch from zod to class-validator for oAuth client

* refactor: switch from zod to class-validator for booking

* refactor: rename dtos folder to input folder

* fix: create migration for prisma schema oauth clients table

* feature: post oAuth client

* refactor: rename oAuth client_secret to secret

* stricter ts config + oauth secret

* remove migrations

* fix: improve response type

* improve error code types

* fix: prettier by removing eslint as its not needed

* refactor: eslint fix files

* wip organization oauth_lcient

* fix import types

* fix team schema platformOAuthClient name

* fix: import packages transpiled

* feat: roles guard

* refactor: use organisationId instead of userId

* fix: jwt secret signing not having jwt_secret

* fix: stop importing input DTOs as types

* fix: jwt sign expecting plain object

* add sentry, routes logger, generic response type

* fix test config

* fixup! fix test config

* refactor: dont await async function return

* feat: fallback value for getEnv

* feat: add helmet for security

* feat: organization guard

* refactor: check if user set in RolesGuard

* refactor: remove roles requirement for oauth read

* chore: add exceptions filters

* fix: api key strategy throws errors

* chore: set e2e tests

* chore: centralize api type in package

* feat: user repository relations

* feat: user repository relations

* Revert "feat: user repository relations"

This reverts commit 1802b256b5a6e5c9b6a62138c8cb1b6f75ed401b.

* Revert "feat: user repository relations"

This reverts commit 4a8f52583d95ff15b6254694c976d8ec9ce0e546.

* test: mock next auth

* fixup! test: mock next auth

* test:oAuth  authed user but no org

* refactor: remove unused type

* refactor: organisation guard based on Membership table

* refactor: folder and file structure

* fix: small changes exception filters

* refactor: merge organization and roles guards into one guard

* fix: remove old export

* fix: org roles guard

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>

* chore: OAuth clients tests and fixes (#12762)

* chore: add tests and fixes

* fixup! chore: add tests and fixes

* fixup! fixup! chore: add tests and fixes

* fixup! fixup! fixup! chore: add tests and fixes

* fixup! fixup! fixup! fixup! chore: add tests and fixes

* fixup! fixup! fixup! fixup! fixup! chore: add tests and fixes

* fixup! fixup! fixup! fixup! fixup! fixup! chore: add tests and fixes

* fix: remove unused imports

* chore: move repos to feature folder (#12769)

* chore: rename repositories

* fix yarn.lock

* feat: oauth client guard (#12814)

* feat: oAuth client guard

* refactor test

* refactor: move oauth-client guard to oauth module

* refactor: separate jest config from package.json

* fix: resolving paths in jest tests

* fix: tests

* jest setup file

* fix: jest test warnings about .js platform constants imports

* refactor: test repository fixtures

* remove allowjs

* ignore js files in ts-jest

* make oauth client module global

* make oauth client module global

* feat: platform oAuth clients frontend (#12867)

* add oauth client to settings

* fix imports

* add react and axios

* oauth client form and card components

* hooks for oauth clients data

* index page for oauth clients

* oauth client list component

* oauth client form page

* shift atoms into platform

* init platform folder

* refactor handleSubmit functioin

* platform

* platform

* platform parts

* revert tsconfig constant platform

* fix: useOauthClients

* feat: create oauth client with api

* fix: add prettier to platform type package

* fixup! fix: add prettier to platform type package

* chore: class-validator types in platform package

* add types for delete oauth client iput

* add onSuccess and onError methods

* update oauth client card view with client id and secret

* cleanup comments

* split oauth persisit hook into create and delete hooks

* fix: oauth client creation / deletion / listing

* fixup! fix: oauth client creation / deletion / listing

* fix: comment logo for now

* fix: layout setting org keys

* cleanup comments

* minor style fixes, add logic for client permissions

* show toast after deleting client

* not passing clint logo at the moment

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>

* chore(platform): OAuth Flow (#12798)

* fixup! chore(platform): OAuth Flow (#12798)

* feat: CRUD for oAuth users (#12853)

* user controller with oauth guard

* crud repository and controller operations

* add user controller to user module

* refactor: oauth guard

* connect user with oauth client upon creation

* fix: test

* wip: e2e test

* refactor: findUniqueOrThrow -> findUnique user

* test: POST user request

* feat: permissions guard

* e2e tests

* e2e

* e2e refactor

* e2e refactor

* reflector decorator check refactor

* refactor oauth guard

* remov unused imports

* log message

* delete permissions decorator and guard

* remove delete user endpoint

* remove delete user endpoint

* refactor: route structure

* remove get oauth client decorator

* delete unecessary e2e config changes

* remove set header in post test

* fix: oauth guard test on empty db

* revert: add previously removed constant to fulfill merge platform

* fix: import to satisfy platform branch merge

* use real implementation of access token guard

* generate access & refresh tokens and fix e2e

* fix: oauth client e2e test

* refactor: variable naming

* refactor

* rename test file

* remove oauth client from request

* refactor: v2 API (#12913)

* Use Boolean only instead of git add src/modules/auth/guard/organization-roles/organization-roles.guard.ts

* move tests next to files they test

* replace .. in import paths with absolute path

* camelCase instead of snake_case for access and refresh token variables

* user sanitize function Typescript friendly

* restructure oAuth clients folder: example for other folders

* restructure bookings module

* organize modules in auth, endpoints, repositories, services

* organize auth module

* organize repositories

* organize inputs

* rename OAuthClientGuard to OAuthClientCredentialsGuard

* add error messages

* add error messages

* clientId as param in oauth-flow & schema mapping

* camelCase instead of snake_case for clientId and clientSecret

* access token guard as passport strategy

* folder structure as features

* get rid of index files

* feat: endpoint for deleting oAuth users & oAuth users returned data (#12912)

* feat: delete oAuth users

* check if access token matches userId in parameter

* driveby: return only user id and email in oauth users endpoints

* refactor: access token errors (#12932)

* change error messages

* error message if access token does not exist in our database

* token expired 498

* access token expired message as constant

* store 498 status code as const

* fix: access token error less information given

* chore: invalid access token const

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>

* feat: external gcal (#12954)

* wip connect gcal

* feat: add external google calendar

* fixup! feat: add external google calendar

* fix: google calendar oauth check invalid

* google cal check tests

* use zod

* fix: await validate access token service

* chore: e2e tests for oAuth flow (#13005)

* fix: accessing length of undefined

* refactor: GetUser throw error if no user provided

* fix: cascade delete PlatformAuthorizationToken if owner or client deleted

* test: POST /authorize

* refactor oauth-flow controller

* refactor oauth-flow controller

* new function to get authorization token by client user ids

* refactor token service

* fix: re-created access and refresh tokens having not unique secret

* oauth flow tests

* oauth flow tests

* feat: abstracted jwt service (#13016)

* remove unused JwtModule from the auth module

* feat: create abstracted jwt service

* refactor: tokens module and service use new jwt service

* refactor: oauth-client module and repository use new jwt service

* implement Morgans requests

* Basic atoms in barebone example platform apps (#13006)

* example app

* example app

* dev move

* fix: more entry points

* fixup! fix: more entry points

* refactor: v2 API (#12913)

* Use Boolean only instead of git add src/modules/auth/guard/organization-roles/organization-roles.guard.ts

* move tests next to files they test

* replace .. in import paths with absolute path

* camelCase instead of snake_case for access and refresh token variables

* user sanitize function Typescript friendly

* restructure oAuth clients folder: example for other folders

* restructure bookings module

* organize modules in auth, endpoints, repositories, services

* organize auth module

* organize repositories

* organize inputs

* rename OAuthClientGuard to OAuthClientCredentialsGuard

* add error messages

* add error messages

* clientId as param in oauth-flow & schema mapping

* camelCase instead of snake_case for clientId and clientSecret

* access token guard as passport strategy

* folder structure as features

* get rid of index files

* feat: endpoint for deleting oAuth users & oAuth users returned data (#12912)

* feat: delete oAuth users

* check if access token matches userId in parameter

* driveby: return only user id and email in oauth users endpoints

* Connect CalProvider and GCal

* Connect CalProvider and GCal

* return response interceptor to handle failed requests

* handle failed requests using axios intercepter

* cal provider refresh tokens, external gcal

* external gcal

* cal provider refresh and retries

* remove console.log

* refactor

* ignore built atoms css

* remove change to token repo

* refactor

* refactor

* downdgrade vite of unrelated packages

* move gcal endpoints to platform

* gcal service

* refactor: use atoms provider

---------

Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in>

* feat: user schedule management (#13053)

* platform-constants package: list of accepted schedule timezones

* feat: schedules endpoint to create schedule with default availability

* refactor: rename function

* refactor: store userId for availabilities

* feat: createSchedule endpoint

* feat: get schedules/default

* feat: getSchedule by id

* feat: get all schedules

* feat: delete schedule

* feat: update schedule

* check user owns schedule

* empty test

* define returned data on controller level not repository

* define returned data on controller level not repository

* Revert "define returned data on controller level not repository"

This reverts commit 4c292a0f0762f58bdcad0707ad8eef7408390800.

* use luxton

* put availabilities out of ee

* use guard on controller level

* refactor

* e2e test schedule creation

* remove log

* test

* default schedule get test

* update schedule test

* delete schedule test

* fix update test

* different email for schedules e2e

* driveby: fix yarn test

* schedule inputs availabilities as array

* re-use BaseStrategy class

* feat: me endpoint v2 api (#13274)

* feat: me endpoint v2 api

* simplify

* chore: NestJS throttling (#13011)

* fix: v2 throttling setup (#13402)

* fix: v2 throttling

* fix: setup redis locally

* move sentry files to api v1

* chore: calcom platform libraries (#13444)

* fixup! chore: calcom platform libraries (#13444)

* fix: fix managed users

* feat: user timezone management in `CalProvider` (#13361)

* hooks to fetch and update user timezone

* add react query package

* add logic to check for change in user timezone and update it

* hook to handle timezone changes

* shift timezone handling logic into separate custom hook

* rename variables

* rename hook to get a user

* call useTimezone hook inside of cal provider

* minor refactors

* add variables for api endpoints

* use URL constructor for creating endpoints

* key prop not needed

* use URL constructor for creating endpoints

* chore: package json scripts

* fix(atoms): fix import path of useUpdateUserTimezone

* fix(atoms): build constants and fix ENDPOINTS const

* feat: v2 api event type endpoints (#13449)

* build platform-libraries

* make getEventTypeById compatible with default PrismaClient

* feat: fetch event type endpoint

* handle event type not found

* e2e test for GET

* test

* determine is user org admin for getEventTypeId correctly

* prisma type like before

* add negative test

* POST event-type and return event-type for api and atom

* POST event-type and return event-type for api and atom

* feat: create default user event types when creating managed user

* feat: v2 api me PUT endpoint (#13486)

* add timeZone to me endpoint

* feat: me endpoint PUT

* reset package.json

* standardize creation and updating of user

* user creation: capitalize timezones and week start

* default event type constants in const file

* refactor: defaultTimezone Europe/London in response schema

* refactor: validators

* refactor: default schedule in schedules endpoints

* refactor: patch instead of put

* fix: getEventTypeById organizationId (#13554)

* chore: transpile more libraries functions

* feat: overlay calendar busy dates events (#13581)

* feat: overlay calendar busy times events

* fixup! feat: overlay calendar busy times events

* fixup! fixup! feat: overlay calendar busy times events

* fixup! fixup! fixup! feat: overlay calendar busy times events

* refactor: schedules for atom (#13585)

* schedules get.handler transformers as util functions

* export schedule transformers from platform-libraries package

* schedules for atom

* refactor: schedules response service format response

* remove unused injected dependency

* fix: prevent duplicate calendar account linking (#13310)

* fix: prevent signing up multiple times from same account

* revert: lark calendar changes

* credential clean up if duplicate

* fix code duplication and check before credential creation

* feat: useRouterQuery allow unset queryParam

* feat: showToast on account duplication attempt

* Small tweak to copy

* Updated other calendars not to use checkDuplicateCalendar

* Add account already linked to apps/installed/calendar

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* Revert event-type profile based querying (#13588)

* test: Bookings: Add more automated tests for organization (#13576)

* Avoid selecting unused props

* Add automated tests

* Add existing user invite and booking

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* fix: organization user shouldn't be asked for payment for a premium username (#13535)

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* chore: refactor handle new reccuring booking (#13597)

* chore: refactor handle new reccuring booking

* fixup! chore: refactor handle new reccuring booking

* fixup! fixup! chore: refactor handle new reccuring booking

* test: Create unit tests for the questions (teste2e-multiSelectQuestion) (#11569)

* Remove unnecessary changes

* add changes

* Requested changes

* Requested changes

* FIx failing tests

* FIx failing tests

* Update regularBookings.ts

* FIx failing tests

* Refactor

* add unit tests for all questions

---------

Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* fix: update valid_for_secs (#13604)

* fix: Fixes teams UI load issue (#13593)

* fixes teams uiload issue

* revert: yarn.lock

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>

* chore: upgrades boxyhq jackson (#13477)

* chore: upgrades boxyhq jackson

* Update jackson.ts

reduces bundle size

* Update yarn.lock

* Update apps/web/next.config.js

* Upgrades again

---------

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

* upgrades copycat

* feat: PLA-47 Reserve Slots Handler (#13607)

* PLA-44 feat: Platform getPublicEvent (#13596)

* chore: getPublicEvent

* chore: fix

* feat: booker atom bookings platform endpoints (#13613)

* feat: handle new booking

* wip

* feat: handle recurring and instant booking

* fixup! feat: handle recurring and instant booking

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: v2 not running (#13636)

* fix: import from platform-libraries instead of lib

* fix: typescript error

* fix: provide EventTypesRepository to SlotsService

* fix: small change

* Merge branch 'main' into platform

* fix: make v2 run after user model changes (#13653)

* feat: event type atom user connected and destination calendars endpoint (#13559)

* extract connected calendars logic in re-usable platform-libraries function

* extract connected calendars logic in re-usable platform-libraries function

* extract connected calendars logic in re-usable platform-libraries function

* calendars endpoints

* enable calendars endpoint

* calendars type form platform-libraries instead of lib

* finish merging platform branch

* fix dbWrite

* fix: issue after merge platform

* fix: new abstracted function export

* change ee/overlay-calendars to ee/calendars endpoint

* refactor: merge overlay service with calendars service

* ts fix

* body -> query

* chore: Platform get schedule (#13696)

* chore: get available slots

* chore: infer event type isTeam

* chore: nit

* chore: Delete selected slot (#13692)

Co-authored-by: Morgan Vernay <morgan@cal.com>

* feat: v2 endpoint for availability atom timezones (#13687)

* refactor: move cityTimezones handler to lib

* test: incorrect timezone when creating oauth user

* refactor: standardize time zone validation

* feat: schedules/time-zones endpoint returning possible time zones

* refactor: @IsValidTimezone load allowed timezones dynamically

* chore: split timezones to its own trpc router

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>

* fix after merge main

* feat: platform useGetPublicEvent (#13752)

* feat: platform useGetPublicEvent

* chore: move public event type to libraries

* chore: use const for query key

* fixup CalendarBusyTimesInput optional params

* feat: platform use get available slots (#13756)

* feat: platform use get available slots

* fixup! feat: platform use get available slots

* feat: platform use get connected calendars (#13757)

* chore: platform enable cors (#13774)

* chore: enable cors

* fixup! chore: enable cors

* feat: platform use get calendars busy times (#13759)

* fixup! feat: platform use get calendars busy times (#13759)

* feat: platform use slots hooks (#13785)

* feat: platform use slots hooks

* fixup! feat: platform use slots hooks

* fixup! fixup! feat: platform use slots hooks

* fixup! fixup! fixup! feat: platform use slots hooks

* fixup! fixup! fixup! fixup! feat: platform use slots hooks

* fixup! feat: platform use slots hooks (#13785)

* feat: platform use create boookings hooks (#13797)

* feat:  `Availability Settings` atom (#13762)

* styling and updating the examples app

* add custom hooks for availability settings atom

* update default exports

* minor updates

* update cal provider to use react query

* availability settings atom

* dialog from shadcn

* navbar for examples app

* add select skeleton loader in calcom ui packages

* refactors

* define rtl for cal provider

* fox trpc call to avoid merge conflicts

* fix issues caused by merges

* revert changes for rtl

* hook to fetch every city timezones

* implement useGetCityTimezones into timezone component

* minor fix

* update hooks

* add props and intergrate into avaialibility component

* add missing await

* remove logs

* add comments

* invalidate queries after settled

* toaster from shadcn

* add toaster into cal provider

* implement toast on event handlers

* add classnames prop to accept custom styles

* passing custom styles

* update packages

* remove comments

* fix: styling

* fix: useUpdateSchedule

* fixup! fix: useUpdateSchedule

* wip

* feat: availability setting atom

* fixup! feat: availability setting atom

* fixup! fixup! feat: availability setting atom

* fixup! fixup! fixup! feat: availability setting atom

* fixup! fixup! fixup! fixup! feat: availability setting atom

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>

* fix: atoms import

* chore: lock

* fix: types

* fixup! fix: types

* refactor: v2 user timezones (#13944)

* fix: OAuth client form improvements (#13837)

* rename app to apps

* fix type errors, validate redirect url and other fixes

* enable user to add multiple redirect uris

* add id to input to make labels clickable

* translations for oauth form

* improve styles, add translations and add select all button for permissions

* add permissions for profile read and write

* fixes

* nit

* use `useFieldArray` for dynamic fields

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* refactor: check organizationId primarily on user profile (#13969)

* refactor: attach movedToProfile to user

* refactor: check organizationId primarily on user profile

* fix: resolve TS errors to build v2 for prod (#13858)

* feat: atoms typescript build (#13864)

* fix: atoms typescript build

* refactor: dayjs

* refactor: meticulous

* refactor: use vite for type generation

* tsconfig

* feat: given user timezone update update default schedule timezone (#13981)

* refactor: standardize schedules api response

* fix: schedules e2e tests

* refactor: use IsTimeZone class-validator instead of custom built one

* feat: validate managed user timezone

* feat: update default schedule when me schedule updated

* refactor: standardize v2 api returned data (#13984)

* feat: V2 api swagger (#13804)

* feat: v2 API swagger docs

* fix: query param not showing up in docs

* latest swagger.json

* refactor: remove scaffolded sample endpoint

* separate internal endpoints

* oauth-clients and oauth flow docs only in dev

* oauth-clients and oauth flow docs only in dev

* create oauth client response doc

* responses for oauth-clients

* document development only endpoints

* chore: consume auth token on exchange (#13993)

* feat: docker build for v2 (#13918)

* fix: resolve TS errors to build v2 for prod

* WIP: api v2 docker

* arg env dockerfile

* wip

* Revert "wip"

This reverts commit 232adf5b969ef004ae907f3e85b12c2715d9b6bc.

* wip

* wip

* fix docker

* fix docker

* fix docker

* docker

* docker

* expose port 80

* feat: oauth client permissions guard (#14020)

* feat: Permissions guard

* feat: Permissions guard

* tests

* feat: use permissions guard for event types

* extra permissions test

* fix tests

* bokings, gcal, me and schedules permissions

* chore: use availability settings atom and fix useTimezone platform hook (#13934)

* replace availabiilty settings with atom component

* add more props for custom styles

* replace intl with dayjs

* review feedback fixes

* fix: v2 api typescript errors (#14059)

* fix: start:prod script

* fix: v2 me module dependencies (#14073)

* feat: check v2 request origin (#14074)

* feat: check v2 request origin

* drive by: remove console log in test

---------

Co-authored-by: Lauris <lauris@Lauriss-Laptop.local>

* fix date overrides typing issues (#14091)

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* fix: v2 e2e tests (#14088)

* fix: v2 e2e tests

* fix: yarn e2e tests run 1 after another

* fix gcal test

---------

Co-authored-by: Lauris <lauris@Lauriss-Laptop.local>

* fix: availability atom and access token origin strategy

* fix: availability atom get error from unknown

* feat: booker atom platform wrapper (#14036)

* feat: booker atom, fix hooks

* wip

* feat: booker atom platform wrapper

* fix conflicts

* fix: build v2 internal dependencies upon v2 build

* fix typing of useDeleteOAuth

* fix: isplatform timezone select improt booker event meta

* fixup! Merge branch 'platform' into feat-booker-atom-platform

* refactor

* add video call event types

* disable email and set managed user names

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>

* update yarn.lock

* Update .prettierignore

* update post install

* fix: post install

* fix yarn lock

* fix: post install

* fix: types

* feat: enable user control over timezone changes and helpers for query params (#14049)

* helpers to get and set value of a query paramater

* add event handlers and props to figure out the timezone preference of a user

* make onTimeZoneChange handler optional

* fixup

* fix merge conflicts

* fixup

* revert changes

* fixup

* fixup

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* fix: usetimezone import

* fix: prisma client import

* fix: prisma client import

* fix: prisma client import

* remove post install using vite

* ts fix attempt: pin prisma to same version

* Revert "ts fix attempt: pin prisma to same version"

This reverts commit 2c309f52e3a881e2dbda4185f45e77967bef6925.

* resolve v2 prisma to calcom prisma

* sync v2 and typescript deps

* sync atoms package

* lock file updat

* fix: platform web components move out of pages folder

* ui: allow experimental decorators to fix ts ci error

* app-store: allow experimental decorators to fix ts ci error

* web: allow experimental decorators to fix ts ci error

* fix: github actions v2 api secrets

* add license

* mock useIsPlatform for tests

* fix timezone select tests

* mock useLocale

* fix tests

* fix tests

* fix tests

* fix github actions

* chore: temporarily disable redis (#14142)

* fix: only one migration file for platform

* fix api v1 files location

* fix: disable apiv2 test in ci

---------

Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Erik <erik@erosemberg.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com>
Co-authored-by: Manpreet Singh <manpoffc@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com>
Co-authored-by: Mike Zhou <mikezhoudev@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com>
Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com>
Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Anant Jain <75206987+anantJjain@users.noreply.github.com>
Co-authored-by: Lauris <lauris@Lauriss-Laptop.local>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
2024-03-20 13:23:19 +02:00

1193 lines
43 KiB
TypeScript

import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { useSession } from "next-auth/react";
import { usePathname, useRouter } from "next/navigation";
import type { RefObject } from "react";
import { createRef, useRef, useState } from "react";
import type { ControlProps } from "react-select";
import { components } from "react-select";
import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { AvailableTimesHeader } from "@calcom/features/bookings";
import { AvailableTimes } from "@calcom/features/bookings";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences";
import DatePicker from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { weekdayToWeekIndex } from "@calcom/lib/date-fns";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Button,
ColorPicker,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
HorizontalTabs,
Label,
Select,
showToast,
Switch,
TextField,
TimezoneSelect,
} from "@calcom/ui";
import { ArrowLeft, Sun } from "@calcom/ui/components/icon";
import { getDimension } from "./lib/getDimension";
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types";
type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined;
const enum Theme {
auto = "auto",
light = "light",
dark = "dark",
}
const queryParamsForDialog = [
"embedType",
"embedTabName",
"embedUrl",
"eventId",
"namespace",
"date",
"month",
];
function useRouterHelpers() {
const router = useRouter();
const searchParams = useCompatSearchParams();
const pathname = usePathname();
const goto = (newSearchParams: Record<string, string>) => {
const newQuery = new URLSearchParams(searchParams ?? undefined);
Object.keys(newSearchParams).forEach((key) => {
newQuery.set(key, newSearchParams[key]);
});
router.push(`${pathname}?${newQuery.toString()}`);
};
const removeQueryParams = (queryParams: string[]) => {
const params = new URLSearchParams(searchParams ?? undefined);
queryParams.forEach((param) => {
params.delete(param);
});
router.push(`${pathname}?${params.toString()}`);
};
return { goto, removeQueryParams };
}
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return (
<components.Control {...props}>
<Sun className="text-subtle mr-2 h-4 w-4" />
{children}
</components.Control>
);
};
const ChooseEmbedTypesDialogContent = ({ types }: { types: EmbedTypes }) => {
const { t } = useLocale();
const { goto } = useRouterHelpers();
return (
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
<h3 className="font-cal text-emphasis mb-2 text-2xl font-semibold leading-none" id="modal-title">
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3>
<div>
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
</div>
</div>
<div className="items-start space-y-2 md:flex md:space-y-0">
{types.map((embed, index) => (
<button
className="hover:bg-subtle bg-muted w-full self-stretch rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
key={index}
data-testid={embed.type}
onClick={() => {
goto({
embedType: embed.type,
});
}}>
<div className="bg-default order-none box-border flex-none rounded-md border border-solid dark:bg-transparent dark:invert">
{embed.illustration}
</div>
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
</button>
))}
</div>
</DialogContent>
);
};
const EmailEmbed = ({
eventType,
username,
orgSlug,
isTeamEvent,
}: {
eventType?: EventType;
username: string;
orgSlug?: string;
isTeamEvent: boolean;
}) => {
const { t, i18n } = useLocale();
const [timezone] = useTimePreferences((state) => [state.timezone]);
useInitializeBookerStore({
username,
eventSlug: eventType?.slug ?? "",
eventId: eventType?.id,
layout: BookerLayouts.MONTH_VIEW,
org: orgSlug,
isTeamEvent,
});
const [month, selectedDate, selectedDatesAndTimes] = useBookerStore(
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
shallow
);
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot] = useBookerStore(
(state) => [
state.setSelectedDate,
state.setMonth,
state.setSelectedDatesAndTimes,
state.setSelectedTimeslot,
],
shallow
);
const event = useEvent();
const schedule = useScheduleForEvent({ orgSlug });
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
const onTimeSelect = (time: string) => {
if (!eventType) {
return null;
}
if (selectedDatesAndTimes && selectedDatesAndTimes[eventType.slug]) {
const selectedDatesAndTimesForEvent = selectedDatesAndTimes[eventType.slug];
const selectedSlots = selectedDatesAndTimesForEvent[selectedDate as string] ?? [];
if (selectedSlots?.includes(time)) {
// Checks whether a user has removed all their timeSlots and thus removes it from the selectedDatesAndTimesForEvent state
if (selectedSlots?.length > 1) {
const updatedDatesAndTimes = {
...selectedDatesAndTimes,
[eventType.slug]: {
...selectedDatesAndTimesForEvent,
[selectedDate as string]: selectedSlots?.filter((slot: string) => slot !== time),
},
};
setSelectedDatesAndTimes(updatedDatesAndTimes);
} else {
const updatedDatesAndTimesForEvent = { ...selectedDatesAndTimesForEvent };
delete updatedDatesAndTimesForEvent[selectedDate as string];
setSelectedTimeslot(null);
setSelectedDatesAndTimes({
...selectedDatesAndTimes,
[eventType.slug]: updatedDatesAndTimesForEvent,
});
}
return;
}
const updatedDatesAndTimes = {
...selectedDatesAndTimes,
[eventType.slug]: {
...selectedDatesAndTimesForEvent,
[selectedDate as string]: [...selectedSlots, time],
},
};
setSelectedDatesAndTimes(updatedDatesAndTimes);
} else if (!selectedDatesAndTimes) {
setSelectedDatesAndTimes({ [eventType.slug]: { [selectedDate as string]: [time] } });
} else {
setSelectedDatesAndTimes({
...selectedDatesAndTimes,
[eventType.slug]: { [selectedDate as string]: [time] },
});
}
setSelectedTimeslot(time);
};
const slots = useSlotsForDate(selectedDate, schedule?.data?.slots);
if (!eventType) {
return null;
}
return (
<div className="flex flex-col">
<div className="mb-[9px] font-medium">
<Collapsible open>
<CollapsibleContent>
<div className="text-default text-sm">{t("select_date")}</div>
<DatePicker
isPending={schedule.isPending}
onChange={(date: Dayjs | null) => {
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
}}
onMonthChange={(date: Dayjs) => {
setMonth(date.format("YYYY-MM"));
setSelectedDate(date.format("YYYY-MM-DD"));
}}
includedDates={nonEmptyScheduleDays}
locale={i18n.language}
browsingDate={month ? dayjs(month) : undefined}
selected={dayjs(selectedDate)}
weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)}
eventSlug={eventType?.slug}
/>
</CollapsibleContent>
</Collapsible>
</div>
{selectedDate ? (
<div className="mt-[9px] font-medium ">
{selectedDate ? (
<div className="flex h-full w-full flex-col gap-4">
<AvailableTimesHeader date={dayjs(selectedDate)} />
<AvailableTimes
className="w-full"
selectedSlots={
eventType.slug &&
selectedDatesAndTimes &&
selectedDatesAndTimes[eventType.slug] &&
selectedDatesAndTimes[eventType.slug][selectedDate as string]
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
: undefined
}
onTimeSelect={onTimeSelect}
slots={slots}
showAvailableSeatsCount={eventType.seatsShowAvailabilityCount}
event={event}
/>
</div>
) : null}
</div>
) : null}
<div className="mb-[9px] font-medium ">
<Collapsible open>
<CollapsibleContent>
<div className="text-default mb-[9px] text-sm">{t("duration")}</div>
<TextField
disabled
label={t("duration")}
defaultValue={eventType?.length ?? 15}
addOnSuffix={<>{t("minutes")}</>}
/>
</CollapsibleContent>
</Collapsible>
</div>
<div className="mb-[9px] font-medium ">
<Collapsible open>
<CollapsibleContent>
<div className="text-default mb-[9px] text-sm">{t("timezone")}</div>
<TimezoneSelect id="timezone" value={timezone} isDisabled />
</CollapsibleContent>
</Collapsible>
</div>
</div>
);
};
const EmailEmbedPreview = ({
eventType,
emailContentRef,
username,
month,
selectedDateAndTime,
calLink,
}: {
eventType: EventType;
timezone?: string;
emailContentRef: RefObject<HTMLDivElement>;
username?: string;
month?: string;
selectedDateAndTime: { [key: string]: string[] };
calLink: string;
}) => {
const { t } = useLocale();
const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]);
if (!eventType) {
return null;
}
return (
<div className="flex h-full items-center justify-center border p-5 last:font-medium">
<div className="border bg-white p-4">
<div
style={{
paddingBottom: "3px",
fontSize: "13px",
color: "black",
lineHeight: "1.4",
minWidth: "30vw",
maxHeight: "50vh",
overflowY: "auto",
backgroundColor: "white",
}}
ref={emailContentRef}>
<div
style={{
fontStyle: "normal",
fontSize: "20px",
fontWeight: "bold",
lineHeight: "19px",
marginTop: "15px",
marginBottom: "15px",
}}>
<b style={{ color: "black" }}> {eventType.title}</b>
</div>
<div
style={{
fontStyle: "normal",
fontWeight: "normal",
fontSize: "14px",
lineHeight: "17px",
color: "#333333",
}}>
{t("duration")}: <b style={{ color: "black" }}>{eventType.length} mins</b>
</div>
<div>
<b style={{ color: "black" }}>
<span
style={{
fontStyle: "normal",
fontWeight: "normal",
fontSize: "14px",
lineHeight: "17px",
color: "#333333",
}}>
{t("timezone")}: <b style={{ color: "black" }}>{timezone}</b>
</span>
</b>
</div>
<b style={{ color: "black" }}>
<>
{selectedDateAndTime &&
Object.keys(selectedDateAndTime)
.sort()
.map((key) => {
const selectedDate = dayjs(key).tz(timezone).format("dddd, MMMM D, YYYY");
return (
<table
key={key}
style={{
marginTop: "16px",
textAlign: "left",
borderCollapse: "collapse",
borderSpacing: "0px",
}}>
<tbody>
<tr>
<td style={{ textAlign: "left", marginTop: "16px" }}>
<span
style={{
fontSize: "14px",
lineHeight: "16px",
paddingBottom: "8px",
color: "rgb(26, 26, 26)",
fontWeight: "bold",
}}>
{selectedDate}
&nbsp;
</span>
</td>
</tr>
<tr>
<td>
<table style={{ borderCollapse: "separate", borderSpacing: "0px 4px" }}>
<tbody>
<tr style={{ height: "25px" }}>
{selectedDateAndTime[key]?.length > 0 &&
selectedDateAndTime[key].map((time) => {
const bookingURL = `${WEBSITE_URL}/${username}/${eventType.slug}?duration=${eventType.length}&date=${key}&month=${month}&slot=${time}`;
return (
<td
key={time}
style={{
padding: "0px",
width: "64px",
display: "inline-block",
marginRight: "4px",
marginBottom: "4px",
height: "24px",
border: "1px solid #111827",
borderRadius: "3px",
}}>
<table style={{ height: "21px" }}>
<tbody>
<tr style={{ height: "21px" }}>
<td style={{ width: "7px" }} />
<td
style={{
width: "50px",
textAlign: "center",
marginRight: "1px",
}}>
<a
href={bookingURL}
className="spot"
style={{
fontFamily: '"Proxima Nova", sans-serif',
textDecoration: "none",
textAlign: "center",
color: "#111827",
fontSize: "12px",
lineHeight: "16px",
}}>
<b
style={{
fontWeight: "normal",
textDecoration: "none",
}}>
{dayjs.utc(time).tz(timezone).format(timeFormat)}
&nbsp;
</b>
</a>
</td>
</tr>
</tbody>
</table>
</td>
);
})}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
);
})}
<div style={{ marginTop: "13px" }}>
<a
className="more"
data-testid="see_all_available_times"
href={`${eventType.bookerUrl}/${calLink}`}
style={{
textDecoration: "none",
cursor: "pointer",
color: "black",
}}>
{t("see_all_available_times")}
</a>
</div>
</>
</b>
<div
className="w-full text-right"
style={{
borderTop: "1px solid #CCCCCC",
marginTop: "8px",
paddingTop: "8px",
}}>
<span>{t("powered_by")}</span>{" "}
<b style={{ color: "black" }}>
<span> Cal.com</span>
</b>
</div>
</div>
<b style={{ color: "black" }} />
</div>
</div>
);
};
const EmbedTypeCodeAndPreviewDialogContent = ({
embedType,
embedUrl,
tabs,
namespace,
eventTypeHideOptionDisabled,
types,
}: {
embedType: EmbedType;
embedUrl: string;
tabs: EmbedTabs;
namespace: string;
eventTypeHideOptionDisabled: boolean;
types: EmbedTypes;
}) => {
const { t } = useLocale();
const searchParams = useCompatSearchParams();
const pathname = usePathname();
const { goto, removeQueryParams } = useRouterHelpers();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dialogContentRef = useRef<HTMLDivElement>(null);
const emailContentRef = useRef<HTMLDivElement>(null);
const { data } = useSession();
const [month, selectedDatesAndTimes] = useBookerStore(
(state) => [state.month, state.selectedDatesAndTimes],
shallow
);
const eventId = searchParams?.get("eventId");
const parsedEventId = parseInt(eventId ?? "", 10);
const calLink = decodeURIComponent(embedUrl);
const { data: eventTypeData } = trpc.viewer.eventTypes.get.useQuery(
{ id: parsedEventId },
{ enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false }
);
const teamSlug = !!eventTypeData?.team ? eventTypeData.team.slug : null;
const s = (href: string) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined);
const [a, b] = href.split("=");
_searchParams.set(a, b);
return `${pathname?.split("?")[0] ?? ""}?${_searchParams.toString()}`;
};
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
tabs
.filter((tab) => tab.type === "code")
.forEach((codeTab) => {
embedCodeRefs[codeTab.name] = createRef();
});
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
const embed = types.find((embed) => embed.type === embedType);
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
const [previewState, setPreviewState] = useState<PreviewState>({
inline: {
width: "100%",
height: "100%",
},
theme: Theme.auto,
layout: BookerLayouts.MONTH_VIEW,
floatingPopup: {},
elementClick: {},
hideEventTypeDetails: false,
palette: {
brandColor: "#000000",
},
});
const close = () => {
removeQueryParams(["dialog", ...queryParamsForDialog]);
};
// Use embed-code as default tab
if (!searchParams?.get("embedTabName")) {
goto({
embedTabName: "embed-code",
});
}
if (!embed || !embedUrl) {
close();
return null;
}
const addToPalette = (update: (typeof previewState)["palette"]) => {
setPreviewState((previewState) => {
return {
...previewState,
palette: {
...previewState.palette,
...update,
},
};
});
};
const previewInstruction = (instruction: { name: string; arg: unknown }) => {
iframeRef.current?.contentWindow?.postMessage(
{
mode: "cal:preview",
type: "instruction",
instruction,
},
"*"
);
};
const inlineEmbedDimensionUpdate = ({ width, height }: { width: string; height: string }) => {
iframeRef.current?.contentWindow?.postMessage(
{
mode: "cal:preview",
type: "inlineEmbedDimensionUpdate",
data: {
width: getDimension(width),
height: getDimension(height),
},
},
"*"
);
};
previewInstruction({
name: "ui",
arg: {
theme: previewState.theme,
layout: previewState.layout,
hideEventTypeDetails: previewState.hideEventTypeDetails,
styles: {
branding: {
...previewState.palette,
},
},
},
});
const handleCopyEmailText = () => {
const contentElement = emailContentRef.current;
if (contentElement !== null) {
const range = document.createRange();
range.selectNode(contentElement);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
selection.removeAllRanges();
}
showToast(t("code_copied"), "success");
}
};
if (embedType === "floating-popup") {
previewInstruction({
name: "floatingButton",
arg: {
attributes: {
id: "my-floating-button",
},
...previewState.floatingPopup,
},
});
}
if (embedType === "inline") {
inlineEmbedDimensionUpdate({
width: previewState.inline.width,
height: previewState.inline.height,
});
}
const ThemeOptions = [
{ value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" },
];
const layoutOptions = [
{ value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") },
{ value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") },
{ value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") },
];
const FloatingPopupPositionOptions = [
{
value: "bottom-right",
label: "Bottom right",
},
{
value: "bottom-left",
label: "Bottom left",
},
];
return (
<DialogContent
enableOverflow
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
type="creation">
<div className="flex">
<div className="bg-muted flex h-[90vh] w-1/3 flex-col overflow-y-auto p-8">
<h3
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
id="modal-title">
<button
className="h-6 w-6"
onClick={() => {
removeQueryParams(["embedType", "embedTabName"]);
}}>
<ArrowLeft className="mr-4 w-4" />
</button>
{embed.title}
</h3>
<h4 className="text-subtle mb-6 text-sm font-normal">{embed.subtitle}</h4>
{eventTypeData?.eventType && embedType === "email" ? (
<EmailEmbed
eventType={eventTypeData?.eventType}
username={teamSlug ?? (data?.user.username as string)}
orgSlug={data?.user?.org?.slug}
isTeamEvent={!!teamSlug}
/>
) : (
<div className="flex flex-col">
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleContent className="text-sm">
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left mb-6 flex items-center !font-normal ">
<div className="mr-[9px]">
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
<TextField
labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height}
required
onChange={(e) => {
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
height,
},
};
});
}}
addOnLeading={<>H</>}
/>
</div>
</div>
<div
className={classNames(
"items-center justify-between",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2 text-sm">Button text</div>
{/* Default Values should come from preview iframe */}
<TextField
labelProps={{ className: "hidden" }}
onChange={(e) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonText: e.target.value,
},
};
});
}}
defaultValue={t("book_my_cal")}
required
/>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup"
? "text-emphasis space-x-2 rtl:space-x-reverse"
: "hidden"
)}>
<Switch
defaultChecked={true}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
hideButtonIcon: !checked,
},
};
});
}}
/>
<div className="text-default my-2 text-sm">Display calendar icon</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2">Position of button</div>
<Select
onChange={(position) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonPosition: position?.value,
},
};
});
}}
defaultValue={FloatingPopupPositionOptions[0]}
options={FloatingPopupPositionOptions}
/>
</div>
<div className="mt-3 flex flex-col xl:flex-row xl:justify-between">
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div className="whitespace-nowrap">Button color</div>
<div className="mt-2 w-40 xl:mt-0 xl:w-full">
<ColorPicker
className="w-[130px]"
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
},
};
});
}}
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div className="whitespace-nowrap">Text color</div>
<div className="mb-6 mt-2 w-40 xl:mt-0 xl:w-full">
<ColorPicker
className="w-[130px]"
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
},
};
});
}}
/>
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<div className="font-medium">
<Collapsible
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleContent>
<div className="text-sm">
<Label className="mb-6">
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
{!eventTypeHideOptionDisabled ? (
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
hideEventTypeDetails: checked,
};
});
}}
/>
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
</div>
) : null}
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label className="mb-6">
<div className="mb-2">{t("layout")}</div>
<Select
className="w-full"
defaultValue={layoutOptions[0]}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
const config = {
...(previewState.floatingPopup.config ?? {}),
layout: option.value,
};
return {
...previewState,
floatingPopup: {
config,
},
layout: option.value,
};
});
}}
options={layoutOptions}
/>
</Label>
</div>
</CollapsibleContent>
</Collapsible>
</div>
</div>
)}
</div>
<div className="flex w-2/3 flex-col px-8 pt-8">
<HorizontalTabs
data-testid="embed-tabs"
tabs={embedType === "email" ? parsedTabs.filter((tab) => tab.name === "Preview") : parsedTabs}
linkShallow
/>
{tabs.map((tab) => {
if (embedType !== "email") {
return (
<div
key={tab.href}
className={classNames(
searchParams?.get("embedTabName") === tab.href.split("=")[1]
? "flex flex-grow flex-col"
: "hidden"
)}>
<div className="flex h-[55vh] flex-grow flex-col">
{tab.type === "code" ? (
<tab.Component
namespace={namespace}
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={refOfEmbedCodesRefs.current[tab.name]}
/>
) : (
<tab.Component
namespace={namespace}
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={iframeRef}
/>
)}
</div>
<div
className={
searchParams?.get("embedTabName") === "embed-preview" ? "mt-2 block" : "hidden"
}
/>
<DialogFooter className="mt-10 flex-row-reverse gap-x-2" showDivider>
<DialogClose />
{tab.type === "code" ? (
<Button
type="submit"
onClick={() => {
const currentTabCodeEl = refOfEmbedCodesRefs.current[tab.name].current;
if (!currentTabCodeEl) {
return;
}
navigator.clipboard.writeText(currentTabCodeEl.value);
showToast(t("code_copied"), "success");
}}>
{t("copy_code")}
</Button>
) : null}
</DialogFooter>
</div>
);
}
if (embedType === "email" && (tab.name !== "Preview" || !eventTypeData?.eventType)) return;
return (
<div key={tab.href} className={classNames("flex flex-grow flex-col")}>
<div className="flex h-[55vh] flex-grow flex-col">
<EmailEmbedPreview
calLink={calLink}
eventType={eventTypeData?.eventType}
emailContentRef={emailContentRef}
username={teamSlug ?? (data?.user.username as string)}
month={month as string}
selectedDateAndTime={
selectedDatesAndTimes
? selectedDatesAndTimes[eventTypeData?.eventType.slug as string]
: {}
}
/>
</div>
<div
className={searchParams?.get("embedTabName") === "embed-preview" ? "mt-2 block" : "hidden"}
/>
<DialogFooter className="mt-10 flex-row-reverse gap-x-2" showDivider>
<DialogClose />
<Button
onClick={() => {
handleCopyEmailText();
}}>
{embedType === "email" ? t("copy") : t("copy_code")}
</Button>
</DialogFooter>
</div>
);
})}
</div>
</div>
</DialogContent>
);
};
export const EmbedDialog = ({
types,
tabs,
eventTypeHideOptionDisabled,
}: {
types: EmbedTypes;
tabs: EmbedTabs;
eventTypeHideOptionDisabled: boolean;
}) => {
const searchParams = useCompatSearchParams();
const embedUrl = (searchParams?.get("embedUrl") || "") as string;
const namespace = (searchParams?.get("namespace") || "") as string;
return (
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
{!searchParams?.get("embedType") ? (
<ChooseEmbedTypesDialogContent types={types} />
) : (
<EmbedTypeCodeAndPreviewDialogContent
embedType={searchParams?.get("embedType") as EmbedType}
embedUrl={embedUrl}
namespace={namespace}
tabs={tabs}
types={types}
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
/>
)}
</Dialog>
);
};
type EmbedButtonProps<T> = {
embedUrl: string;
namespace: string;
children?: React.ReactNode;
className?: string;
as?: T;
eventId?: number;
};
export const EmbedButton = <T extends React.ElementType>({
embedUrl,
children,
className = "",
as,
eventId,
namespace,
...props
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
const { goto } = useRouterHelpers();
className = classNames("hidden lg:inline-flex", className);
const openEmbedModal = () => {
goto({
dialog: "embed",
eventId: eventId ? eventId.toString() : "",
namespace,
embedUrl,
});
};
const Component = as ?? Button;
return (
<Component
{...props}
className={className}
data-test-embed-url={embedUrl}
data-testid="embed"
type="button"
onClick={() => {
openEmbedModal();
}}>
{children}
</Component>
);
};