Compare commits

...
107 Commits
Author SHA1 Message Date
Alex van Andel b83ee2d57d v2.9.2
release.yml / release (push) Canceled after 0s
2023-05-23 02:32:41 +02:00
Shivam KalraandPeer Richelsen 5f39bbe730 feat: mirotalk app-store setup (#9010)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-23 00:00:47 +00:00
Peer Richelsen cf3d8cb4bb fixed password input widht (#9049) 2023-05-22 23:30:54 +00:00
fbe0018d01 fix: cancelled booking doesn't show the booker (#8203)
* fix: remove attendee deletion

* chore: add test id

* test: add e2e

* Fix data-testid placeholder

* Fix prettier error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-22 23:15:06 +00:00
6586ed4d87 change bg of date-input field (#9043)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-22 23:03:13 +00:00
Nafees Nazik 81e57afcf4 fix: user undefined (#9022) 2023-05-22 23:02:54 +00:00
Alex van Andel 5cc530593b Update lockfile
release.yml / release (push) Canceled after 0s
2023-05-23 00:26:22 +02:00
Nafees Nazik 4111f78431 feat: make branding hideable in the new booker (#9033)
* feat: make branding hideable

* fix: typo
2023-05-22 21:54:43 +00:00
Udit Takkar def80ea4cf fix: reject icon color (#9031)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-05-22 21:53:37 +00:00
2fc4676f6d fix: [CAL-1726] UI: seats input focus too wide (#8999)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-22 18:02:49 +00:00
Peer Richelsen 318bafaab8 Update LICENSE 2023-05-22 18:56:17 +01:00
Peer Richelsen fc10c29037 Update LICENSE 2023-05-22 18:54:09 +01:00
Riyan MohammadandUdit Takkar ccb7b1f9ac Optimize conditional rendering (#9009)
* Optimize conditional rendering

Declared a variable called isAdmin. The check session.data?.user.role === "ADMIN" is repeated multiple times. You can store this value in a variable to avoid redundant checks.

* Update AppsLayout.tsx

Removed extra line space to match prettier format

* fix: lint error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-05-22 13:51:22 +00:00
Peer Richelsen c6c6544f50 Update package.json 2023-05-22 14:21:25 +01:00
Hariom Balhara 8a6b93a747 Fix: Embed - Auto theme configured using embed config doesn't work when there is non-system theme configured in appearance. (#9030)
* Differentiate b/w auto theme and unspecified theme

* Fix TS
2023-05-22 11:17:56 +00:00
alannnc ff6c6ccec0 Replacing bookingAttendees for bookingUid (#8997) 2023-05-22 09:30:24 +00:00
c8db177f7d fix: team member search error (#8982)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-05-22 09:29:28 +00:00
Nafees Nazikandalannnc 8871e1eb21 fix: wrong locale in attendee email ics (#8945)
* fix: pass attendee translator function

* fix: type

* fix: t

* fix: t

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-22 06:14:44 +00:00
Shivam KalraandPeer Richelsen 92b2d57871 feat: 8x8 app-store setup (#9015)
* feat: 8x8 app-store setup

* fix: regex remove optional protocol, -imageSrc

* Update packages/app-store/eightxeight/config.json

* Update packages/app-store/eightxeight/config.json

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
2023-05-21 18:21:30 +00:00
Alex van Andel 77623c72c5 Bump lockfile 2023-05-20 12:52:55 +02:00
3239976e18 Fix: Profile update refresh issues (#8570)
* profile update refresh fixes

* updated avatar based on logged in user

* lint & types check fixes

* fixed settings layout to fetch udpated session

* pushing the 8448 changes to separate PR

* removing hook not required

* Removed extra code

---------

Co-authored-by: Keyur Patel <keyur.patel@difx.io>
Co-authored-by: Alan <alannnc@gmail.com>
2023-05-20 00:37:58 +00:00
611939ffe8 remove todo to issue action (#8903)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-19 22:53:11 +00:00
1b8ecbe449 [CAL-1008] Fix 'All Bookings' filter not getting selected after coming from 'Your Bookings' (#8902)
* [CAL-1008] Fix 'All Bookings' filter not getting selected after coming from 'Your Bookings'

* Remove optional param from removeAllQueryParams function

* Remove setQuery from the extraction

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-19 22:50:57 +00:00
cd4882429d fix: impersonation switch active state (#8924)
* fix: switch

* fix: checked

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-19 21:23:12 +00:00
zomars 4fd2d80637 Linting 2023-05-19 14:07:25 -07:00
a343d934fe Replacement PR for closed PR #8960 (#8980)
* fix focus ring on number of seats

* Adding yarn.lock back

* checkout yarn.lock from main

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-19 20:30:30 +00:00
Hariom Balharaandalannnc 933cc739bd Add config support in floatingButton api (#8986)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-05-19 20:10:19 +00:00
Sanu Dutta c7c75ff4d3 enable search in select and adding border bottom color (#8988)
* searchable select

* divider color subtle
2023-05-19 19:43:01 +00:00
Kamalika 41f1920a7a Borders Fixed with border-subtle (#8990) 2023-05-19 16:43:41 +00:00
Carina WollendorferandCarinaWolli e4836ea8f3 make sure to also trigger when cancelled is null (#8992)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-05-19 14:41:44 +00:00
Hariom Balhara bfa30aa504 Implement UpgradeTip in App install flow (#8968) 2023-05-19 09:52:17 +00:00
Carina WollendorferandCarinaWolli b36043b637 Fixes SMS workflow actions for premium users (#8983)
* fix sms actions for users with premium user names

* code clean up

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-05-19 08:10:11 +00:00
ASHOK-SINGH 75026289fd Fix- Calendar override with the wrong dates and time (#8936) 2023-05-19 07:22:26 +00:00
Parth Sharma 80649c9f1a fix height issue (#8973) 2023-05-19 06:46:06 +00:00
Isaiah Hamilton 7dd75afb63 fix button width (#8979) 2023-05-19 06:25:44 +00:00
Crowdin Bot f66e4aeeed New Crowdin translations by Github Action 2023-05-19 00:45:20 +00:00
Rama Krishna Reddyandrkreddy99 8225e61710 fix redirect after adding ping.gg (#8972)
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
2023-05-19 00:22:07 +00:00
Jeroen Reumkens 175fc4fcf3 CAL-1673: Use dark/light colors from user profile settings in booker (#8905)
* CAL-1673: Use dark/light colors from user profile settings in booker

* Use system theme for dynamic booking, instead of theme set by first user.
2023-05-18 06:16:51 +00:00
shaharyarshamshiandJoe Au-Yeung f20a5df322 Guide to setup enviroment variable added in contributing mardown file (#8911)
* Guide to setup enviroment variable added in contributing mardown file

* linting error

* Update CONTRIBUTING.md

* linting error

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-05-17 15:11:28 -07:00
62544931b2 updated language of raycast instructions (#8908)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-17 19:10:25 +01:00
Alex van Andel f6ad1d6ed5 Bump lockfile 2023-05-17 16:25:03 +02:00
d22b1072b5 change datepicker styles to fix dark mode issue (#8886)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-05-17 13:12:52 +00:00
4b356dd937 chore: extended slugify function (#8740)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2023-05-17 13:05:14 +00:00
1f51199a7b issue#8601: fix (#8713)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-05-17 12:57:12 +00:00
Crowdin Bot b80a631998 New Crowdin translations by Github Action 2023-05-17 13:15:49 +00:00
Crowdin Bot bef4422080 New Crowdin translations by Github Action 2023-05-17 13:14:27 +00:00
cfd1a72c64 [CAL-1545] replace heroicons with lucide.dev (#8599)
* fix linting

* [CAL-1545] replace heroicons with lucide.dev

* [CAL-1545] replace heroicons with lucide.dev

* [CAL-1545] replace heroicons with lucide.dev

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-05-17 12:47:44 +00:00
23b3a6661c Add feature to offset start times for event types (#8506)
* Add offsetStart column to EventType

* Update buildSlots to support offset start times

* Add "Offset Start Time" to edit event types form

* Fix offset events not appearing on availability selector

* Guard against negative offsetStart values

* Lock offsetStart field for managed event types

* EventLimits UI tweaks for "Offset start times"

* EventLimits UI: Fix offsetStart preview not always updating

* Remove unnecessary ctx from getSchedule.test.ts

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-17 11:56:55 +00:00
Hariom Balhara b8b6c48d7d Improvements: Prefill Routing Forms and connect prefilling with Booking Form (#8780)
* Support prefilling routing form and prefilling Booking form through routing form

* Use Option Value as is instead of lowercasing

* Fix prefill validation issue

* Add prefill tests

* Fix Routing Form tests

* Small fix
2023-05-17 08:47:48 +00:00
Crowdin Bot 81655f9988 New Crowdin translations by Github Action 2023-05-17 08:47:19 +00:00
Nafees Nazik ff74822c48 Fix: locale and time format in attendee email (#8941)
* feat: add locale prop

* feat: add locale prop

* feat: add locale prop

* fix: time format
2023-05-17 08:17:09 +00:00
Jeroen Reumkens 8ec178debd CAL-1591: Remember form values in new booker form when user navigates back and forth between timeslots and form (#8920) 2023-05-16 20:34:15 +00:00
Hariom BalharaandOmar López f881061d8d Fully handle booking pages with conflicting themes being opened together (#8921)
* Handle booking pages with conflicting themes being opened together

* Add comments

---------

Co-authored-by: Omar López <zomars@me.com>
2023-05-16 19:41:47 +00:00
Crowdin Bot 2525559d6b New Crowdin translations by Github Action 2023-05-16 11:27:38 +00:00
Carina WollendorferandCarinaWolli 38fb7a3f77 fix locked toggle (#8926)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-05-16 11:04:03 +00:00
Alex van Andel 08956e54c5 Fix yarn.lock (missing lockfile commit in #8769) 2023-05-16 00:51:24 +02:00
ea65a2a27e fix: add exp in /meeting-tokens (#8869)
* fix: add exp in /meeting-tokens

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* hijacking this PR to add a NIT change lol

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-15 20:15:05 +00:00
Crowdin Bot 0d45cfb7ea New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-05-15 16:13:08 +00:00
Alex van Andel a4fc25e260 v2.9.0 2023-05-15 18:12:07 +02:00
Jeroen ReumkensandAlex van Andel 796c932311 CAL-1550: Align form submit button of new booker to bottom (#8896)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-05-15 15:02:16 +00:00
Jeroen Reumkens 4137f63ec7 CAL-1664: Improve flash of toggle group when rerendering (#8900)
* CAL-1664: Improve flash of toggle group when rerendering, because active element moves into position.

* Added back newline
2023-05-15 14:38:02 +00:00
Jeroen Reumkens 2147cc82a5 CAL-1557: Improve timeformat selector alignment in new booker (#8898) 2023-05-15 14:37:23 +00:00
Crowdin Bot 40e646e9a4 New Crowdin translations by Github Action 2023-05-15 14:22:54 +00:00
cb2225259c Enabling workflows for managed events (#8446)
* Enabling workflows for managed events

* Disabling fields for workflow when readonly

* Disabling event type workflows if readonly

* Installing auth app

* Reverting yarn.lock

* Again reverting yarn

* Showing user workflows in managed event type

* Reusing existing code to show all workflows

* Further extending workflow support

* Added unit test

* Fixing workflow assignment

* Fixing locked workflows for children MET

* Update packages/features/ee/workflows/components/EventWorkflowsTab.tsx

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Personal workflows fix, matching count numbers

* Fixing menu items for managed event types

* Fixing type checks

* Fixing empty activeOn select

* Fixing unit test

* handling active workflows for MET

* Fixing MET slug nit

* Embed option in action restored for non-MET

* Correcting mobile view when hidden

* More adjustments

* fix sms reminder field for children

* remove console.log

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-05-15 13:56:26 +00:00
372cd94d9f Gitpod setup And Bio Detail Overflow Bug Fixed (#8824)
* feat: Add Gitpod setup and update README.md

* fix: Fix overflow issue in bio details with word-wrap property

* minor changes in gitpod script

* updated gitpod url for cal

* gitpod url fixed in markdown

* fix: lint errors

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* added vscode extensions and  enabled prebuilds to reduce waiting time

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-05-15 13:18:31 +00:00
Vedant Jainandsean-brydon c7c282f30f [fix] : select options marginal space (#8892)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-05-15 10:25:33 +00:00
959488910f fix: strip markdown from meta description tag in users page (#8769)
* fix: remove markdown validation

* feat: use markdown stripped bio

* fix: use remove markdown

* fix: type errors

---------

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-15 07:44:23 +00:00
Hariom Balhara 0ca0cc3265 Add eventTypeSelected event (#8894) 2023-05-15 06:30:08 +00:00
69bc12187e Fixed dark mode bugs in the availability section (#8630)
* Fixed dark mode bugs in the availability section

* Reverted the tooltip change for delete button

* Updated tooltip of delete button conditionally based on isLastSchedule flag

* fix: disabled button state

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-05-14 17:46:19 +00:00
Abdurrahman RajabandCarina Wollendorfer 5e772530b2 Feat: select all days in schedule (#8796)
* feat: add select all

* fix: fix deleted days

* chore: add i18n select all

* chore: check all days when select all

* chore: use calcom checkbox

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-05-14 16:00:55 +00:00
Peer Richelsen 3088b1174a only border top for bottom nav (#8882) 2023-05-14 11:06:10 +00:00
Santosh Kuchetti fadf12b2bd Star not centered issue #8519 (#8872)
* modifications for the issues #8159 are done

* modifications for the issue #8519 are made
2023-05-13 09:51:16 +00:00
sean-brydon c81f30b092 Fix hidden label breaking layout (#8867) 2023-05-13 09:50:00 +00:00
Hariom Balharaandzomars 476119d627 Add changesets/cli to release embed (#8126)
* Add dep versions

* Release new version for embeds

* Add prepublish

* Use prepare script which works with both npm and yarn

* Add changesets correctly

* Add embed publishing command

---------

Co-authored-by: zomars <zomars@me.com>
2023-05-13 08:22:31 +00:00
Crowdin Bot 040f03a9b5 New Crowdin translations by Github Action 2023-05-13 04:31:26 +00:00
Shivang SharmaandOmar López b552bf6956 Fix: #8528 Moved the data checks to superRefine (#8749)
* Fix: #8528 Moved the data checks to superRefine

* Resolved lint isseu

* Updated to parse from safeParse

* added new line

---------

Co-authored-by: Omar López <zomars@me.com>
2023-05-13 04:11:12 +00:00
82d3c4370d cancel and schedule reminders when workflow activated/deactivated on event type (#8757)
* add switches to advanced even type settings

* only show switches when workflow is enabled

* check in event type update handler if user is allowed to disable standard emails

* don't send emails if disabled

* remove  reminders when workflow is deactivated on event type

* make sure emails can't be disabled if no workflow exist

* send workflow emails to all attendees

* code clean up

* add translations

* always send to all attendees when scheduling reminders

* schedule reminders if event type gets activated on workflow

* schedule reminders (commented code)

* change text

* schedule reminders for all attendees not just first one

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-05-12 17:31:01 +00:00
492b4d3e97 Refactor/readme.md (#8723)
* Update README.md 

Removed 
- Heroku DB setup section (since it's no longer free). 
Added
- Section for setting up local DB.
- Some other minor details.

* Minor grammer fixes

---------

Co-authored-by: vaibhav bisht <vaibhav@aus.co.in>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-05-12 10:39:03 -06:00
0ae319d206 a11y: Exit the lexical Editor and button a11y on tab index (#8139)
* a11y: Exit the lexical Editor and button a11y on tab index

Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>

* chore: Updated lexical to v0.9.0

Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>

* chore: Added ErrorBoundary for Lexical RichTextPlugin

Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>

* chore: yarn install changes

Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>

* fix: merge conflicts

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Update yarn.lock

---------

Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-05-12 10:28:14 -06:00
Hariom Balhara 749311be2a Pull latest main in every submodule (#8537)
* Update to latest main so that yarn.lock is always correct

* Update git-setup.sh
2023-05-12 10:20:32 -06:00
Crowdin Bot 3cce4e5009 New Crowdin translations by Github Action 2023-05-12 15:46:20 +00:00
Mendy Landa 341f82ccb9 chore: removed duplicate value in className @ Tooltip.tsx (#8865) 2023-05-12 09:45:32 -06:00
Pranjal Goyal e028a339f5 Adding icon.svg and renaming config file (#8859) 2023-05-12 12:57:09 +00:00
sean-brydonandBailey Pumfleet c55e432bc3 Feat/team invite flow (#8804)
* completeOnboarding drives invite flow changes

* Username prefil - force getting started when acc doesnt exist

* Change subtitle text and unique error on multiple invites

* User exists and no password set - generate signup link still

* Fix callback param

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

* Fix spacing

* Add password hints

* Fix translations

---------

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-05-12 12:52:09 +00:00
Hariom BalharaandJoe Au-Yeung f47c3cd538 Fix/ Routing Form Routes (#8827)
* Reuse split routes

* Add cache

* Update formMutation.handler.ts

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-05-12 12:01:05 +00:00
Nafees Nazik 4b70a20941 fix: overflow (#8858) 2023-05-12 10:55:11 +00:00
Hariom Balhara c873b83965 Unstage enums file (#8836) 2023-05-12 05:02:42 +00:00
Hariom Balhara ff859737ec Improvement/AppStore: Remove seeding from app-store-cli. (#8486)
* Remove seeding from cli

* Self review fixes

* Fix TS error
2023-05-12 04:59:15 +00:00
AnsariandPeer Richelsen 607ef71f9f fix: teams upgrade banner on smaller screens (#8786)
* background image fix 📲

* object image position change 🖼

* Duplicate Deleted Field 🎭

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-12 00:23:11 +00:00
24dd4002fa fixed color of status bar for pwa (#8583)
* fixed color of status bar for pwa

* Apply suggestions from code review

* Apply suggestions from code review

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-11 23:50:11 +00:00
Praveen DiasandJoe Au-Yeung 078c9ddca5 Improved API key error message for SendGrid setup page (#8847)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-05-11 23:04:10 +00:00
alannnc b44e5a7aae Fix no video reference on google-meet (#8849) 2023-05-11 22:49:10 +00:00
4374da08e2 #8701 CAL-1621 new booker dialog grow in height (#8774)
* increase scrollable height

* Fix react-select version - import esm error

* Increase booking form modal height

* Update packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx

* Match design specs

* nit change

* Trying to fix type errors

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-05-11 17:31:51 +00:00
Crowdin Bot 98b531e6b5 New Crowdin translations by Github Action 2023-05-11 16:26:01 +00:00
74414db760 feat: add meta and seo tags for new booker component (#8697)
* feat: add hidden to select

* feat: add hide branding to store

* fix: remove branding from store

* feat: prefetch data

* feat: use meta tags

* fix: remove branding from store

* fix: prefetching

* refactor: useEvent hook

* fix: type

* fix :booker seo

* fix: type error

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
2023-05-11 16:02:13 +00:00
sean-brydon bdf3e34ea1 Feat/onboarding video step connection (#8838)
* UI work - WIP scrollable area

* Add translations - refactor some components

* Add installed text

* Disable if there is no currently installed

* Extract loader

* Fix conditional

* Fix E2E

* fix typo
2023-05-11 13:20:39 +00:00
Jeroen Reumkens 738e0f3e50 CAL-1553: Show dates for multi occurence event instead of input to change occurences, during booking step. Also make form less wide and meta a bit wider during booker step (in small view only), so form looks better and dates fit on a single line for the multiple occurences. When split over multiple lines they would otherwise take up a lot of unneccesary space. (#8834) 2023-05-11 11:51:41 +00:00
Jeroen Reumkens 16ae19abdb CAL-1624: Timezone select layout improvements when typing (#8832) 2023-05-11 08:19:26 +00:00
a2e18ae9ba deleted fields in duplicate form 🎭 (#8808)
* deleted fields in duplicate form  🎭

* Fixed duplicate deleted fields

* Update packages/app-store/routing-forms/trpc/_router.ts

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-05-11 07:42:37 +00:00
e2cfd0d1d1 Webex integration (#7651)
* start webex app creation

* webex integration wip

* fix lint errors

* fix lint errors

* add webex env vars in appStore.example

* webex app wip

* fix lint errors

* edit webex oauth scopes

* add location in webex app config

* add site url placeholder and regex in webex config location

* debug translateEvent

* fix utc formatting for event start time, add test boilerplate for webex, add envs

* fix location and datetime formatting

* get correct videoCredentials for deleteMeeting

* Move webex specific readme content to webex README

* Fix app not visible in app-store

* Delete setup route

* add webex icon

* delete prev icon

* webex api fix

* add app screenshots

* Revert tests changes as they dont run

* Use config instead of hardcoding vales

* Update README

* Remove all env variables related to WEBEX app. They can be added through settings->admin->apps interface

* update from origin

* fix icon path

* update webex readme

* Update yarn.lock

* update webex readme

* Remove unnecessary URL from webex

* revert changes in cancel booking handler

* simply webex zod schemas, logs for debugging

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-05-11 07:14:32 +00:00
Crowdin Bot 067b77faf1 New Crowdin translations by Github Action 2023-05-11 04:07:21 +00:00
Ashray ShettyandAlex van Andel 740c01ac29 align text "Date overrides" with corresponding svg (#8819)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-05-11 03:45:14 +00:00
asadath1395andBailey Pumfleet 97cfd1ffa7 Fix delete icon background color in availability (#8810)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-05-10 15:15:13 +00:00
Jithil P Ponnan a7b2cfc90f fix: cal-1647 alignment issues (#8817) 2023-05-10 15:05:46 +00:00
d067b0782a feat: add RECORDING_READY webhook (#8277)
* feat: add RECORDING_READY webhook

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: type errors and improvements

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-05-10 14:56:31 +00:00
203a36e2b5 Make Jitsi Host configurable (#8636)
* Closes #1847 - adds an environment variable `JITSI_HOST_URL` to configure the Host URL for self hosted instances

* Closes #1847 - adds an environment variable `JITSI_HOST_URL` to configure the Host URL for self hosted instances

* Closes #1847 - adds an environment variable `JITSI_HOST_URL` to configure the Host URL for self hosted instances

* Replace Tab with spaces

* Add JITSI_HOST_URL env

* Remote Tabs for Spaces

* Support jitsi_host key

* Remove ENV, add Interface jitsi host URL and fallback

* Fix linting issues

* Clean code

* add missing await

* Add configurable meeting slug

* Refactor to jitsiHost, remove slug generator

* zod default doesnt work right now. So remove it

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-05-10 14:46:14 +00:00
Hariom Balhara f5a38c3f89 Add missing zoho-bigin import (#8798) 2023-05-10 14:37:16 +00:00
301 changed files with 4789 additions and 2078 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
-28
View File
@@ -1,28 +0,0 @@
name: TODO to Issue
on:
push:
branches:
- main
workflow_dispatch:
inputs:
MANUAL_COMMIT_REF:
description: "The SHA of the commit to get the diff for"
required: true
MANUAL_BASE_REF:
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
todos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Add TODOs to issues"
uses: "alstr/todo-to-issue-action@v4"
env:
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
with:
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻‍♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
+43
View File
@@ -0,0 +1,43 @@
tasks:
- init: |
yarn &&
cp .env.example .env &&
next_auth_secret=$(openssl rand -base64 32) &&
calendso_encryption_key=$(openssl rand -base64 24) &&
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" \
-e "s|^CALENDSO_ENCRYPTION_KEY=.*|CALENDSO_ENCRYPTION_KEY=$calendso_encryption_key|" .env
command: yarn dx
ports:
- port: 3000
visibility: public
onOpen: open-preview
- port: 5420
visibility: private
onOpen: ignore
- port: 1025
visibility: private
onOpen: ignore
- port: 8025
visibility: private
onOpen: ignore
github:
prebuilds:
master: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: true
addBadge: true
vscode:
extensions:
- DavidAnson.vscode-markdownlint
- yzhang.markdown-all-in-one
- esbenp.prettier-vscode
- dbaeumer.vscode-eslint
- bradlc.vscode-tailwindcss
- ban.spellright
- stripe.vscode-stripe
- Prisma.prisma
File diff suppressed because one or more lines are too long
+2
View File
@@ -3,5 +3,7 @@ nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.4.1.cjs
+6 -1
View File
@@ -86,7 +86,12 @@ To develop locally:
yarn
```
5. Start developing and watch for code changes:
5. Set up your `.env` file:
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
6. Start developing and watch for code changes:
```sh
yarn dev
+1 -1
View File
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
Portions of this software are licensed as follows:
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
+27 -19
View File
@@ -158,6 +158,15 @@ yarn dx
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
#### Gitpod Setup
1. Click the button below to open this project in Gitpod.
2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/calcom/cal.com)
#### Manual setup
@@ -166,34 +175,29 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
```
<details>
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku.</summary>
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
1. Create a free account with [Heroku](https://heroku.com).
1. [Download](https://www.postgresql.org/download/) and install postgres in your local (if you don't have it already).
2. Create a new app.
<img width="306" alt="Create an App" src="https://user-images.githubusercontent.com/16905768/115322780-b3d58c00-a17e-11eb-8a52-b758fb0ea942.png">
2. Create your own local db by executing `createDB <DB name>`
3. In your new app, go to `Overview` and next to `Installed add-ons`, click `Configure Add-ons`. We need this to set up our database.
![image](https://user-images.githubusercontent.com/16905768/115323232-a53ba480-a17f-11eb-98db-58e2f8c52426.png)
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
4. Once you clicked on `Configure Add-ons`, click on `Find more add-ons` and search for `postgres`. One of the options will be `Heroku Postgres` - click on that option.
![image](https://user-images.githubusercontent.com/16905768/115323126-5beb5500-a17f-11eb-8030-7380310807a9.png)
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
![image](https://user-images.githubusercontent.com/39329182/236612291-51d87f69-6dc1-4a23-bf4d-1ca1754e0a35.png)
5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`.
<img width="512" alt="Submit Order Form" src="https://user-images.githubusercontent.com/16905768/115323265-b4baed80-a17f-11eb-99f0-d67f019aa6df.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`.
6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`.
![image](https://user-images.githubusercontent.com/16905768/115323367-e92ea980-a17f-11eb-9ff4-dec95f2ec349.png)
7. In `Settings`, copy your URI to your Cal.com `.env` file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
![image](https://user-images.githubusercontent.com/16905768/115323556-4591c900-a180-11eb-9808-2f55d2aa3995.png)
![image](https://user-images.githubusercontent.com/16905768/115323697-7a9e1b80-a180-11eb-9f08-a742b1037f90.png)
8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com).
</details>
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
- [Setup postgres DB with render](https://render.com/docs/databases)
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
@@ -460,6 +464,10 @@ following
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Webex Client ID and Secret
[See Webex Readme](./packages/app-store/webex/)
### Obtaining ZohoCRM Client ID and Secret
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@calcom/docs",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
+10
View File
@@ -0,0 +1,10 @@
# @calcom/web
## 2.7.16
### Patch Changes
- Updated dependencies
- @calcom/embed-snippet@1.0.7
- @calcom/embed-react@1.0.12
- @calcom/embed-core@1.1.5
+7 -1
View File
@@ -36,6 +36,7 @@ const Component = ({
tos,
privacy,
isProOnly,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
@@ -152,6 +153,7 @@ const Component = ({
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -192,6 +194,7 @@ const Component = ({
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -241,7 +244,9 @@ const Component = ({
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{price === 0 ? (
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
@@ -358,6 +363,7 @@ export default function App(props: {
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
@@ -18,9 +18,9 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
const { t } = useLocale();
const session = useSession();
const router = useRouter();
const isAdmin = session.data?.user.role === "ADMIN";
if (session.status === "loading") return <></>;
return (
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<div className="flex flex-col xl:flex-row">
@@ -29,8 +29,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<EmptyScreen
Icon={AlertCircle}
headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
description={isAdmin ? "You can enable apps in the settings" : ""}
buttonText={isAdmin ? t("apps_settings") : ""}
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
/>
) : (
@@ -72,7 +72,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
bookingAttendees: bookingAttendees || undefined,
bookingUid: slot.bookingUid,
});
};
@@ -29,7 +29,7 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
@@ -119,7 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: Slash,
icon: Ban,
disabled: mutation.isLoading,
},
// For bookings with payment, only confirm if the booking is paid for
@@ -237,7 +237,7 @@ const BookingPage = ({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
bookingAttendees: currentSlotBooking ? currentSlotBooking.attendees.length : undefined,
bookingUid: currentSlotBooking?.uid,
});
}
};
@@ -322,7 +322,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
disabled={seatsLocked.disabled}
defaultValue={value || 2}
min={1}
className="w-24"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
@@ -119,7 +119,7 @@ const EventTypeScheduleDetails = ({
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
@@ -108,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
const { t, i18n } = useLocale();
const formMethods = useFormContext<FormValues>();
const PERIOD_TYPES = [
@@ -149,12 +149,25 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const periodTypeLocked = shouldLockDisableProps("periodType");
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
];
// offsetStart toggle is client-side only, opened by default if offsetStart is set
const offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
// Preview how the offset will affect start times
const offsetOriginalTime = new Date();
offsetOriginalTime.setHours(9, 0, 0, 0);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
return (
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
@@ -432,6 +445,32 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</SettingsToggle>
)}
/>
<hr className="border-subtle" />
<SettingsToggle
title={t("offset_toggle")}
description={t("offset_toggle_description")}
{...offsetStartLockedProps}
checked={offsetToggle}
onCheckedChange={(active) => {
setOffsetToggle(active);
if (!active) {
formMethods.setValue("offsetStart", 0);
}
}}>
<TextField
required
type="number"
{...offsetStartLockedProps}
label={t("offset_start")}
defaultValue={eventType.offsetStart}
{...formMethods.register("offsetStart")}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
})}
/>
</SettingsToggle>
</div>
);
};
@@ -390,7 +390,12 @@ export const EventSetupTab = (
addOnLeading={
<>
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{team ? "team/" + team.slug : eventType.users[0].username}/
{!isManagedEventType
? team
? "team/" + team.slug
: eventType.users[0].username
: t("username_placeholder")}
/
</>
}
{...formMethods.register("slug", {
@@ -170,7 +170,7 @@ export const EventTeamWebhooksTab = ({
</Dialog>
{/* Edit webhook dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent title={t("edit_webhook")}>
<DialogContent enableOverflow title={t("edit_webhook")}>
<WebhookForm
webhook={webhookToEdit}
apps={installedApps?.items.map((app) => app.slug)}
@@ -170,7 +170,7 @@ function EventTypeSingleLayout({
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
let navigation = getNavigation({
const navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
@@ -210,7 +210,7 @@ function EventTypeSingleLayout({
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
navigation.splice(-2, 1);
} else {
navigation.push({
name: "webhooks",
@@ -3,15 +3,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
interface ICalendarItem {
interface IAppConnectionItem {
title: string;
description?: string;
logo: string;
type: App["type"];
installed?: boolean;
}
const CalendarItem = (props: ICalendarItem) => {
const { title, logo, type } = props;
const AppConnectionItem = (props: IAppConnectionItem) => {
const { title, logo, type, installed } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center justify-between p-5">
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => {
<Button
{...buttonProps}
color="secondary"
disabled={installed}
type="button"
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
}}>
{t("connect")}
{installed ? t("installed") : t("connect")}
</Button>
)}
/>
@@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => {
);
};
export { CalendarItem };
export { AppConnectionItem };
@@ -0,0 +1,17 @@
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
export function StepConnectionLoader() {
return (
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{Array.from({ length: 4 }).map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</li>
);
})}
</ul>
);
}
@@ -1,13 +1,13 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
import { List } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { CalendarItem } from "../components/CalendarItem";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
interface IConnectCalendarsProps {
nextStep: () => void;
@@ -33,7 +33,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar.integration.title &&
firstCalendar.integration.logo && (
<>
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black ">
<ConnectedCalendarItem
key={firstCalendar.integration.title}
name={firstCalendar.integration.title}
@@ -60,7 +60,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
queryIntegrations.data.items.map((item) => (
<li key={item.title}>
{item.title && item.logo && (
<CalendarItem
<AppConnectionItem
type={item.type}
title={item.title}
description={item.description}
@@ -72,19 +72,8 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryIntegrations.isLoading && (
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{[0, 0, 0, 0].map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</li>
);
})}
</ul>
)}
{queryIntegrations.isLoading && <StepConnectionLoader />}
<button
type="button"
data-testid="save-calendar-button"
@@ -95,7 +84,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
onClick={() => nextStep()}
disabled={disabledNextButton}>
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
@@ -0,0 +1,67 @@
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 { AppConnectionItem } from "../components/AppConnectionItem";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
interface ConnectedAppStepProps {
nextStep: () => void;
}
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { nextStep } = props;
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
});
const { t } = useLocale();
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
(item) => item.credentialIds.length > 0
);
return (
<>
{!isLoading && (
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
{queryConnectedVideoApps?.items &&
queryConnectedVideoApps?.items.map((item) => {
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
return (
<li key={item.name}>
{item.name && item.logo && (
<AppConnectionItem
type={item.type}
title={item.name}
description={item.description}
logo={item.logo}
installed={item.credentialIds.length > 0}
/>
)}
</li>
);
})}
</List>
)}
{isLoading && <StepConnectionLoader />}
<button
type="button"
data-testid="save-video-button"
className={classNames(
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
)}
disabled={!hasAnyInstalledVideoApps}
onClick={() => nextStep()}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
};
export { ConnectedVideoStep };
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
@@ -9,6 +8,7 @@ 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";
interface ISetupAvailabilityProps {
nextStep: () => void;
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
type="submit"
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
disabled={availabilityForm.formState.isSubmitting}>
{t("next_step_text")} <ArrowRightIcon className="ml-2 h-4 w-4 self-center" />
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
</Button>
</div>
</Form>
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import type { FormEvent } from "react";
import { useRef, useState } from "react";
@@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { Avatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
@@ -159,7 +159,7 @@ const UserProfile = (props: IUserProfileProps) => {
type="submit"
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
{t("finish")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
</form>
);
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -10,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
@@ -116,7 +116,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-8 flex w-full flex-row justify-center"
disabled={mutation.isLoading}>
{t("next_step_text")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
</form>
);
+1 -1
View File
@@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
{!isBioEmpty ? (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
/>
</>
@@ -1,4 +1,3 @@
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useRouter } from "next/router";
@@ -15,6 +14,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, Input, Label } from "@calcom/ui";
import { Star as StarSolid } from "@calcom/ui/components/icon";
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum {
@@ -230,12 +230,16 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="absolute top-0 right-2 flex flex-row">
<span
className={classNames(
"mx-2 py-1",
isInputUsernamePremium ? "text-orange-400" : "",
"mx-2 py-2",
isInputUsernamePremium ? "text-transparent" : "",
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] h-4 w-4" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 h-4 w-4" /> : <></>}
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? (
<Check className="mt-[2px] h-4 w-4" />
) : (
<></>
)}
</span>
</div>
</div>
+105 -44
View File
@@ -26,7 +26,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
);
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
export type AppProps = Omit<
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
"Component"
> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean;
@@ -72,58 +75,21 @@ const enum ThemeSupport {
Booking = "userConfigured",
}
const CalcomThemeProvider = (
props: PropsWithChildren<
WithNonceProps & {
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
isThemeSupported?: boolean;
}
>
) => {
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
// undefined or true means we use system theme
type CalcomThemeProps = PropsWithChildren<
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
>;
const CalcomThemeProvider = (props: CalcomThemeProps) => {
const router = useRouter();
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const isEmbedMode = typeof embedNamespace === "string";
const storageKey = isEmbedMode
? `embed-theme-${embedNamespace}`
: themeSupport === ThemeSupport.App
? "app-theme"
: themeSupport === ThemeSupport.Booking
? "booking-theme"
: undefined;
return (
<ThemeProvider
nonce={props.nonce}
enableColorScheme={false}
enableSystem={themeSupport !== ThemeSupport.None}
forcedTheme={forcedTheme}
storageKey={storageKey}
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key={storageKey}
attribute="class">
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
@@ -140,6 +106,100 @@ const CalcomThemeProvider = (
);
};
/**
* The most important job for this fn is to generate correct storageKey for theme persistenc.
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
* So, we handle all the cases here namely,
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
* - Embeds using different namespace. They can be completely themed different on the same page.
* - Embeds using the same namespace but showing different cal.com links with different themes
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
* - App has different theme then Booking Pages.
*
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
*
* Some things to note:
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
* - ... and so on ...
*/
function getThemeProviderProps({
props,
isEmbedMode,
embedNamespace,
router,
}: {
props: Omit<CalcomThemeProps, "children">;
isEmbedMode: boolean;
embedNamespace: string | null;
router: NextRouter;
}) {
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
const themeBasis = props.themeBasis;
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
console.warn(
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
);
}
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
let embedExplicitlySetThemeSuffix = "";
if (typeof window !== "undefined") {
const embedTheme = window.getEmbedTheme();
if (embedTheme) {
embedExplicitlySetThemeSuffix = ":" + embedTheme;
}
}
const storageKey = isEmbedMode
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
return {
storageKey,
forcedTheme,
themeSupport,
nonce: props.nonce,
enableColorScheme: false,
enableSystem: themeSupport !== ThemeSupport.None,
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key: storageKey,
attribute: "class",
};
}
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
@@ -157,6 +217,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}>
-6
View File
@@ -111,12 +111,6 @@ const nextConfig = {
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
preventFullImport: true,
},
"@heroicons/react/solid": {
transform: "@heroicons/react/solid/esm/{{ member }}",
},
"@heroicons/react/outline": {
transform: "@heroicons/react/outline/esm/{{ member }}",
},
"@calcom/features/insights/components": {
transform: "@calcom/features/insights/components/{{member}}",
skipDefaultConversion: true,
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.8.13",
"version": "2.9.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -41,7 +41,6 @@
"@daily-co/daily-js": "^0.37.0",
"@formkit/auto-animate": "^1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4",
@@ -115,12 +114,11 @@
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-use-intercom": "1.5.1",
"remark": "^14.0.2",
"remove-markdown": "^0.5.0",
"rrule": "^2.7.1",
"sanitize-html": "^2.10.0",
"schema-dts": "^1.1.0",
"short-uuid": "^4.2.0",
"strip-markdown": "^5.0.0",
"stripe": "^9.16.0",
"superjson": "1.9.1",
"tailwindcss-radix": "^2.6.0",
@@ -153,6 +151,7 @@
"@types/qrcode": "^1.4.3",
"@types/react": "18.0.26",
"@types/react-phone-number-input": "^3.0.14",
"@types/remove-markdown": "^0.3.1",
"@types/sanitize-html": "^2.9.0",
"@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1",
+22 -9
View File
@@ -1,4 +1,3 @@
import { BadgeCheckIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
@@ -23,11 +22,12 @@ import defaultEvents, {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -37,7 +37,16 @@ import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const {
users,
profile,
eventTypes,
isDynamicGroup,
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
} = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(user.theme);
const { t } = useLocale();
@@ -107,11 +116,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<>
<HeadSeo
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
description={
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
}
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
meeting={{
title: isDynamicGroup ? "" : `${user.bio}`,
title: isDynamicGroup ? "" : markdownStrippedBio,
profile: { name: `${profile.name}`, image: null },
users: isDynamicGroup
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
@@ -131,12 +138,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
<h1 className="font-cal text-emphasis mb-1 text-3xl">
{nameOrUsername}
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
{user.verified && (
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
)}
</h1>
{!isBioEmpty && (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
@@ -342,11 +351,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const safeBio = markdownToSafeHTML(user.bio) || "";
const markdownStrippedBio = stripMarkdown(user?.bio || "");
return {
props: {
users,
safeBio,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroup ? null : user.username,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
@@ -361,6 +373,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
dynamicNames,
dynamicUsernames,
isSingleUser,
markdownStrippedBio,
},
};
};
+4
View File
@@ -177,6 +177,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
slug: `${user.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
},
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: user.username,
away: user?.away,
isDynamic: false,
trpcState: ssg.dehydrate(),
@@ -307,6 +309,8 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
props: {
eventType: eventTypeObject,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: null,
isDynamic: true,
away: false,
trpcState: ssg.dehydrate(),
+2
View File
@@ -293,6 +293,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
props: {
away: user.away,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroupBooking ? null : user.username,
eventType: eventTypeObject,
booking,
currentSlotBooking: currentSlotBooking,
+2 -1
View File
@@ -46,7 +46,8 @@ class MyDocument extends Document<Props> {
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" content="var(--cal-bg)" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
</Head>
<body
+62 -21
View File
@@ -1,3 +1,4 @@
import type { WebhookTriggerEvents } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
@@ -5,6 +6,8 @@ import { DailyLocationType } from "@calcom/app-store/locations";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
@@ -20,6 +23,38 @@ const downloadLinkSchema = z.object({
download_link: z.string(),
});
const triggerWebhook = async ({
evt,
downloadLink,
booking,
}: {
evt: CalendarEvent;
downloadLink: string;
booking: {
userId: number | undefined;
eventTypeId: number | null;
};
}) => {
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
// Send Webhook call if hooked to BOOKING.RECORDING_READY
const subscriberOptions = {
userId: booking.userId ?? 0,
eventTypeId: booking.eventTypeId ?? 0,
triggerEvent: eventTrigger,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
downloadLink,
}).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
})
);
await Promise.all(promises);
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" });
@@ -51,6 +86,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
uid: true,
location: true,
isRecorded: true,
eventTypeId: true,
user: {
select: {
id: true,
@@ -105,31 +141,36 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
});
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
const downloadLinkResponse = downloadLinkSchema.parse(response);
const downloadLink = downloadLinkResponse.download_link;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description || undefined,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking.user?.email || "Email-less",
name: booking.user?.name || "Nameless",
timeZone: booking.user?.timeZone || "Europe/London",
language: { translate: t, locale: booking?.user?.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
};
await triggerWebhook({
evt,
downloadLink,
booking: { userId: booking?.user?.id, eventTypeId: booking.eventTypeId },
});
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
// send emails to all attendees only when user has team plan
if (isSendingEmailsAllowed) {
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
const downloadLinkResponse = downloadLinkSchema.parse(response);
const downloadLink = downloadLinkResponse.download_link;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description || undefined,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking.user?.email || "Email-less",
name: booking.user?.name || "Nameless",
timeZone: booking.user?.timeZone || "Europe/London",
language: { translate: t, locale: booking?.user?.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
};
await sendDailyVideoRecordingEmails(evt, downloadLink);
return res.status(200).json({ message: "Success" });
}
+1 -4
View File
@@ -4,7 +4,6 @@ import type { SatoriOptions } from "satori";
import { z } from "zod";
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
import { md } from "@calcom/lib/markdownIt";
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
res.arrayBuffer()
@@ -75,12 +74,10 @@ export default async function handler(req: NextApiRequest) {
imageType,
});
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
const img = new ImageResponse(
(
<Meeting
title={title_}
title={title}
profile={{ name: meetingProfileName, image: meetingImage }}
users={names.map((name, index) => ({ name, username: usernames[index] }))}
/>
+48 -12
View File
@@ -2,6 +2,7 @@ import fs from "fs";
import matter from "gray-matter";
import MarkdownIt from "markdown-it";
import type { GetStaticPaths, GetStaticPropsContext } from "next";
import Link from "next/link";
import path from "path";
import { z } from "zod";
@@ -33,7 +34,26 @@ const sourceSchema = z.object({
}),
});
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
// If it's not production environment, it would be a better idea to inform that the App is disabled.
if (props.isAppDisabled) {
if (process.env.NODE_ENV !== "production") {
// TODO: Improve disabled App UI. This is just a placeholder.
return (
<div className="p-2">
This App seems to be disabled. If you are an admin, you can enable this app from{" "}
<Link href="/settings/admin/apps" className="cursor-pointer text-blue-500 underline">
here
</Link>
</div>
);
}
// Disabled App should give 404 any ways in production.
return null;
}
const { source, data } = props;
return (
<App
name={data.name}
@@ -52,6 +72,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
website={data.url}
email={data.email}
licenseRequired={data.licenseRequired}
teamsPlanRequired={data.teamsPlanRequired}
isProOnly={data.isProOnly}
descriptionItems={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
@@ -80,29 +101,43 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (typeof ctx.params?.slug !== "string") return { notFound: true };
const app = await prisma.app.findUnique({
const appMeta = await getAppWithMetadata({
slug: ctx.params?.slug,
});
const appFromDb = await prisma.app.findUnique({
where: { slug: ctx.params.slug.toLowerCase() },
});
if (!app) return { notFound: true };
const isAppAvailableInFileSystem = appMeta;
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
const singleApp = await getAppWithMetadata(app);
if (process.env.NODE_ENV !== "production" && isAppDisabled) {
return {
props: {
isAppDisabled: true as const,
data: {
...appMeta,
},
},
};
}
if (!singleApp) return { notFound: true };
if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true };
const isTemplate = singleApp.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
const isTemplate = appMeta.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
const postFilePath = path.join(README_PATH);
let source = "";
try {
source = fs.readFileSync(postFilePath).toString();
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
} catch (error) {
/* If the app doesn't have a README we fallback to the package description */
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
source = singleApp.description;
source = appMeta.description;
}
const result = matter(source);
@@ -111,8 +146,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
data.items = data.items.map((item) => {
if (typeof item === "string") {
return getAppAssetFullPath(item, {
dirName: singleApp.dirName,
isTemplate: singleApp.isTemplate,
dirName: appMeta.dirName,
isTemplate: appMeta.isTemplate,
});
}
return item;
@@ -120,8 +155,9 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
}
return {
props: {
isAppDisabled: false as const,
source: { content, data },
data: singleApp,
data: appMeta,
},
};
};
+1 -1
View File
@@ -172,7 +172,7 @@ export default function Login({
className="mb-0"
{...register("password")}
/>
<div className="absolute -top-1.5 ltr:right-0 rtl:left-0">
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
<Link
href="/auth/forgot-password"
tabIndex={-1}
+6 -4
View File
@@ -157,10 +157,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
let deploymentKey = await getDeploymentKey(prisma);
const deploymentKey = await prisma.deployment.findUnique({
where: { id: 1 },
select: { licenseKey: true },
});
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
await prisma.deployment.upsert({
where: { id: 1 },
update: {
@@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
agreedLicenseAt: new Date(),
},
});
deploymentKey = await getDeploymentKey(prisma);
}
const isFreeLicense = deploymentKey === "";
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
return {
props: {
+4 -4
View File
@@ -1,4 +1,3 @@
import { CheckIcon, MailOpenIcon, ExclamationIcon } from "@heroicons/react/outline";
import { signIn } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -9,6 +8,7 @@ import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
import Loader from "@components/Loader";
import PageWrapper from "@components/PageWrapper";
@@ -118,11 +118,11 @@ export default function Verify() {
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
<div className="rounded-full border border-white p-3">
{hasPaymentFailed ? (
<ExclamationIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
) : sessionId ? (
<CheckIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
) : (
<MailOpenIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
)}
</div>
<h3 className="font-cal my-6 text-3xl font-normal">
+2 -2
View File
@@ -62,7 +62,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
<h3 className="text-emphasis font-medium leading-6">
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block">
<span className="inline-block align-middle">
<Info className="h-4 w-4" />
</span>
</Tooltip>
@@ -212,7 +212,7 @@ export default function Availability() {
aria-label={t("delete")}
className="hidden sm:inline"
disabled={schedule?.isLastSchedule}
tooltip={t("requires_at_least_one_schedule")}
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
/>
</DialogTrigger>
<ConfirmationDialogContent
+1 -1
View File
@@ -53,7 +53,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
{t("overview_of_day")}{" "}
<input
type="date"
className="inline h-8 border-none p-0"
className="inline h-8 border-none bg-inherit p-0"
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(e.target.value);
+5 -2
View File
@@ -443,8 +443,10 @@ export default function Success(props: SuccessProps) {
)}
{bookingInfo?.attendees.map((attendee) => (
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
{attendee.name && <p>{attendee.name}</p>}
<p data-testid={`attendee-${attendee.email}`}>{attendee.email}</p>
{attendee.name && (
<p data-testid={`attendee-name-${attendee.name}`}>{attendee.name}</p>
)}
<p data-testid={`attendee-email-${attendee.email}`}>{attendee.email}</p>
</div>
))}
</div>
@@ -1092,6 +1094,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
+1
View File
@@ -160,6 +160,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
away: user.away,
themeBasis: user.username,
isDynamicGroup: false,
profile,
date,
+1
View File
@@ -122,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
profile,
themeBasis: user.username,
eventType: eventTypeObject,
booking: null,
currentSlotBooking: null,
@@ -51,6 +51,7 @@ export type FormValues = {
eventName: string;
slug: string;
length: number;
offsetStart: number;
description: string;
disableGuests: boolean;
requiresConfirmation: boolean;
@@ -211,6 +212,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookingLimits: eventType.bookingLimits || undefined,
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
offsetStart: eventType.offsetStart,
hidden: eventType.hidden,
periodDates: {
startDate: periodDates.startDate,
@@ -255,6 +257,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
)
.optional(),
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
bookingFields: eventTypeBookingFields,
})
// TODO: Add schema for other fields later.
+12 -10
View File
@@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
</>
)}
{!isManagedEventType && (
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
!isChildrenManagedEventType && (
+42 -3
View File
@@ -15,6 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
import UserProfile from "@components/getting-started/steps-views/UserProfile";
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
@@ -22,7 +23,13 @@ import { UserSettings } from "@components/getting-started/steps-views/UserSettin
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
const INITIAL_STEP = "user-settings";
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
const steps = [
"user-settings",
"connected-calendar",
"connected-video",
"setup-availability",
"user-profile",
] as const;
const stepTransform = (step: (typeof steps)[number]) => {
const stepIndex = steps.indexOf(step);
@@ -36,9 +43,9 @@ const stepRouteSchema = z.object({
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
});
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
const OnboardingPage = (props: IOnboardingPageProps) => {
const router = useRouter();
const { user } = props;
const { t } = useLocale();
@@ -55,6 +62,11 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
subtitle: [`${t("connect_your_calendar_instructions")}`],
skipText: `${t("connect_calendar_later")}`,
},
{
title: `${t("connect_your_video_app")}`,
subtitle: [`${t("connect_your_video_app_instructions")}`],
skipText: `${t("set_up_later")}`,
},
{
title: `${t("set_availability")}`,
subtitle: [
@@ -68,6 +80,17 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
},
];
// TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto
// Ability to accept other pending invites if any (low priority)
// if (props.hasPendingInvites) {
// headers.unshift(
// props.hasPendingInvites && {
// title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`,
// subtitle: [], // TODO: come up with some subtitle text here
// }
// );
// }
const goToIndex = (index: number) => {
const newStep = steps[index];
router.push(
@@ -122,12 +145,15 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
{currentStep === "setup-availability" && (
<SetupAvailability nextStep={() => goToIndex(3)} defaultScheduleId={user.defaultScheduleId} />
<SetupAvailability nextStep={() => goToIndex(4)} defaultScheduleId={user.defaultScheduleId} />
)}
{currentStep === "user-profile" && <UserProfile user={user} />}
</StepCard>
{headers[currentStepIndex]?.skipText && (
<div className="flex w-full flex-row justify-center">
<Button
@@ -181,6 +207,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
allowDynamicBooking: true,
defaultScheduleId: true,
completedOnboarding: true,
teams: {
select: {
accepted: true,
team: {
select: {
id: true,
name: true,
logo: true,
},
},
},
},
},
});
@@ -199,6 +237,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
},
};
};
+29 -8
View File
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getUsernameList } from "@calcom/lib/defaultEvents";
@@ -13,10 +14,22 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away }: PageProps) {
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
@@ -27,8 +40,8 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { user, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssgInit } = await import("@server/lib/ssg");
const ssg = await ssgInit(context);
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const usernameList = getUsernameList(user);
const users = await prisma.user.findMany({
@@ -53,13 +66,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
await ssr.viewer.public.event.prefetch({ username: user, eventSlug: slug });
return {
props: {
booking,
user,
slug,
away: false,
trpcState: ssg.dehydrate(),
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
},
};
}
@@ -67,14 +83,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: username, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssgInit } = await import("@server/lib/ssg");
const ssg = await ssgInit(context);
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findUnique({
where: {
username,
},
select: {
away: true,
hideBranding: true,
},
});
@@ -89,13 +107,16 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
await ssr.viewer.public.event.prefetch({ username, eventSlug: slug });
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssg.dehydrate(),
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
},
};
}
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import prisma from "@calcom/prisma";
@@ -12,10 +13,22 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away }: PageProps) {
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
@@ -30,8 +43,8 @@ const paramsSchema = z.object({ type: z.string(), slug: z.string() });
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssgInit } = await import("@server/lib/ssg");
const ssg = await ssgInit(context);
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const team = await prisma.team.findFirst({
where: {
@@ -39,6 +52,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
select: {
id: true,
hideBranding: true,
},
});
@@ -52,14 +66,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
await ssr.viewer.public.event.prefetch({ username: teamSlug, eventSlug: meetingSlug });
return {
props: {
booking,
away: false,
user: teamSlug,
slug: meetingSlug,
trpcState: ssg.dehydrate(),
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
},
};
};
@@ -50,6 +50,7 @@ const ProfileImpersonationView = () => {
setValue,
reset,
getValues,
watch,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
@@ -63,11 +64,11 @@ const ProfileImpersonationView = () => {
}}>
<div className="flex space-x-3">
<Switch
defaultChecked={!user?.disableImpersonation}
onCheckedChange={(e) => {
setValue("disableImpersonation", !e, { shouldDirty: true });
}}
fitToHeight={true}
checked={!watch("disableImpersonation")}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
+40 -28
View File
@@ -4,10 +4,12 @@ import { useRouter } from "next/router";
import type { CSSProperties } from "react";
import type { SubmitHandler } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
@@ -16,7 +18,6 @@ import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@c
import PageWrapper from "@components/PageWrapper";
import { asStringOrNull } from "../lib/asStringOrNull";
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
import { ssrInit } from "../server/lib/ssr";
@@ -24,11 +25,10 @@ type FormValues = {
username: string;
email: string;
password: string;
passwordcheck: string;
apiError: string;
};
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
export default function Signup({ prepopulateFormValues, token }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const telemetry = useTelemetry();
@@ -76,7 +76,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
return (
<LicenseRequired>
<div
className="bg-muted flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8"
className="bg-muted flex min-h-screen flex-col justify-center "
style={
{
"--cal-brand": "#111827",
@@ -95,7 +95,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-default mx-2 px-4 py-8 shadow sm:rounded-lg sm:px-10">
<div className="bg-default mx-2 p-6 shadow sm:rounded-lg lg:p-8">
<FormProvider {...methods}>
<form
onSubmit={(event) => {
@@ -109,7 +109,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
}}
className="bg-default space-y-6">
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
<div className="space-y-2">
<div className="space-y-4">
<TextField
addOnLeading={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/`}
{...register("username")}
@@ -125,32 +125,28 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
className: "block text-sm font-medium text-default",
}}
{...register("password")}
hintErrors={["caplow", "min", "num"]}
className="border-default mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
<PasswordField
label={t("confirm_password")}
{...register("passwordcheck", {
validate: (value) =>
value === methods.watch("password") || (t("error_password_mismatch") as string),
})}
/>
</div>
<div className="flex space-x-2 rtl:space-x-reverse">
<Button type="submit" loading={isSubmitting} className="w-7/12 justify-center">
<Button type="submit" loading={isSubmitting} className="w-full justify-center">
{t("create_account")}
</Button>
<Button
color="secondary"
className="w-5/12 justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}
</Button>
{!token && (
<Button
color="secondary"
className="w-full justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}
</Button>
)}
</div>
</form>
</FormProvider>
@@ -163,7 +159,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const ssr = await ssrInit(ctx);
const token = asStringOrNull(ctx.query.token);
const token = z.string().optional().parse(ctx.query.token);
const props = {
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
@@ -221,11 +217,27 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
};
}
const guessUsernameFromEmail = (email: string) => {
const [username] = email.split("@");
return username;
};
let username = guessUsernameFromEmail(verificationToken.identifier);
if (!IS_SELF_HOSTED) {
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
const { available, suggestion } = await checkPremiumUsername(username);
username = available ? username : suggestion || username;
}
return {
props: {
...props,
token,
prepopulateFormValues: {
email: verificationToken.identifier,
username,
},
},
};
+14 -4
View File
@@ -4,7 +4,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
@@ -12,6 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
@@ -26,7 +27,7 @@ import Team from "@components/team/screens/Team";
import { ssrInit } from "@server/lib/ssr";
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team, isUnpublished }: TeamPageProps) {
function TeamPage({ team, isUnpublished, markdownStrippedBio }: TeamPageProps) {
useTheme(team.theme);
const showMembers = useToggleQuery("members");
const { t } = useLocale();
@@ -67,6 +68,11 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<div className="px-6 py-4 ">
<Link
href={`/team/${team.slug}/${type.slug}`}
onClick={async () => {
sdkActionManager?.fire("eventTypeSelected", {
eventType: type,
});
}}
data-testid="event-type-link"
className="flex justify-between">
<div className="flex-shrink">
@@ -100,7 +106,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
title={teamName}
description={teamName}
meeting={{
title: team?.bio || "",
title: markdownStrippedBio,
profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) },
}}
/>
@@ -111,7 +117,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
{!isBioEmpty && (
<>
<div
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
@@ -196,10 +202,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
});
const markdownStrippedBio = stripMarkdown(team?.bio || "");
return {
props: {
team: { ...team, safeBio, members },
themeBasis: team.slug,
trpcState: ssr.dehydrate(),
markdownStrippedBio,
},
} as const;
};
+1
View File
@@ -196,6 +196,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
brandColor: team.brandColor,
darkBrandColor: team.darkBrandColor,
},
themeBasis: team.slug,
date: dateParam,
eventType: eventTypeObject,
workingHours,
+1
View File
@@ -153,6 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
image: eventTypeObject.team?.logo || null,
eventName: null,
},
themeBasis: eventTypeObject.team?.slug,
eventType: eventTypeObject,
recurringEventCount,
booking,
+9 -2
View File
@@ -8,6 +8,8 @@ import {
bookTimeSlot,
selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth,
testEmail,
testName,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -101,6 +103,8 @@ testBothBookers.describe("pro user", () => {
// Because it tests the entire booking flow + the cancellation + rebooking
test.setTimeout(testInfo.timeout * 3);
await bookFirstEvent(page);
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
const [pro] = users.get();
await pro.login();
@@ -114,7 +118,10 @@ testBothBookers.describe("pro user", () => {
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
await expect(cancelledHeadline).toBe("This event is cancelled");
expect(cancelledHeadline).toBe("This event is cancelled");
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
await page.goto(`/${pro.username}`);
await bookFirstEvent(page);
@@ -155,7 +162,7 @@ testBothBookers.describe("pro user", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
additionalGuests.forEach(async (email) => {
await expect(page.locator(`[data-testid="attendee-${email}"]`)).toHaveText(email);
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
});
});
});
+12 -6
View File
@@ -419,9 +419,13 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
// Go to cancel page and see that attendees are listed and myself as I'm owner of the booking
await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`);
const foundFirstAttendeeAsOwner = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
const foundFirstAttendeeAsOwner = await page.locator(
'p[data-testid="attendee-email-first+seats@cal.com"]'
);
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);
const foundSecondAttendeeAsOwner = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
const foundSecondAttendeeAsOwner = await page.locator(
'p[data-testid="attendee-email-second+seats@cal.com"]'
);
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
await page.pause();
await page.goto("auth/logout");
@@ -433,10 +437,12 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
);
// No attendees should be displayed only the one that it's cancelling
const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
const notFoundSecondAttendee = await page.locator(
'p[data-testid="attendee-email-second+seats@cal.com"]'
);
await expect(notFoundSecondAttendee).toHaveCount(0);
const foundFirstAttendee = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
const foundFirstAttendee = await page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
await expect(foundFirstAttendee).toHaveCount(1);
await prisma.eventType.update({
@@ -453,11 +459,11 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
);
// Now attendees should be displayed
const foundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
const foundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
await expect(foundSecondAttendee).toHaveCount(1);
const foundFirstAttendeeAgain = await page
.locator('p[data-testid="attendee-first+seats@cal.com"]')
.locator('p[data-testid="attendee-email-first+seats@cal.com"]')
.first();
await expect(foundFirstAttendeeAgain).toHaveCount(1);
});
+5 -2
View File
@@ -15,6 +15,9 @@ type Request = IncomingMessage & { body?: unknown };
type RequestHandlerOptions = { req: Request; res: ServerResponse };
type RequestHandler = (opts: RequestHandlerOptions) => void;
export const testEmail = "test@example.com";
export const testName = "Test Testson";
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
const {
requestHandler = ({ res }) => {
@@ -133,8 +136,8 @@ export async function bookFirstEvent(page: Page) {
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string }) => {
// --- fill form
await page.fill('[name="name"]', opts?.name ?? "Test Testson");
await page.fill('[name="email"]', opts?.email ?? "test@example.com");
await page.fill('[name="name"]', opts?.name ?? testName);
await page.fill('[name="email"]', opts?.email ?? testEmail);
await page.press('[name="email"]', "Enter");
};
// Provide an standalone localize utility not managed by next-i18n
+11 -2
View File
@@ -47,10 +47,19 @@ test.describe("Onboarding", () => {
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
await expect(page).toHaveURL(/.*connected-video/);
});
await test.step("step 3", async () => {
const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
await expect(isDisabled).toBe(true);
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
});
await test.step("step 4", async () => {
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
await expect(isDisabled).toBe(false);
// same here, skip this step.
@@ -59,7 +68,7 @@ test.describe("Onboarding", () => {
await expect(page).toHaveURL(/.*user-profile/);
});
await test.step("step 4", async () => {
await test.step("step 5", async () => {
await page.locator("button[type=submit]").click();
// should redirect to /event-types after onboarding
Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 743 B

+1 -3
View File
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke="white" stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" x2="19.07" y1="4.93" y2="19.07"></line></svg>

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 308 B

@@ -1572,7 +1572,7 @@
"sender_name": "اسم المرسل",
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
"no_recordings_found": "لم يتم العثور على تسجيلات",
"new_workflow_subtitle": "سير عمل جديد من أجل...",
"new_workflow_subtitle": "سير عمل جديد لـ...",
"reporting": "تقديم التقارير",
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
"teams_plan_required": "خطة Teams مطلوبة",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق",
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب",
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
"locked_by_admin": "مقفل من قبل المشرف",
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
"connect_now": "اتصل الآن",
"managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Členové budou vidět aktivní aplikace, ale nastavení aplikací upravovat moci nebudou",
"locked_webhooks_description": "Členové budou vidět aktivní webhooky, ale nastavení webhooků upravovat moci nebudou",
"locked_workflows_description": "Členové budou vidět aktivní pracovní postupy, ale nastavení pracovních postupů upravovat moci nebudou",
"locked_by_admin": "Uzamčeno správcem",
"app_not_connected": "Nemáte připojený účet {{appName}}.",
"connect_now": "Připojte se",
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
"locked_by_admin": "Von Admin gesperrt",
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
"connect_now": "Jetzt verbinden",
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
+12 -1
View File
@@ -237,6 +237,8 @@
"set_availability": "Set your availability",
"continue_without_calendar": "Continue without calendar",
"connect_your_calendar": "Connect your calendar",
"connect_your_video_app":"Connect your video apps",
"connect_your_video_app_instructions":"Connect your video apps to use them on your event types.",
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
"set_up_later": "Set up later",
"current_time": "Current time",
@@ -367,6 +369,7 @@
"create_webhook": "Create Webhook",
"booking_cancelled": "Booking Cancelled",
"booking_rescheduled": "Booking Rescheduled",
"recording_ready":"Recording Download Link Ready",
"booking_created": "Booking Created",
"meeting_ended": "Meeting Ended",
"form_submitted": "Form Submitted",
@@ -753,6 +756,10 @@
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
"length": "Length",
"minimum_booking_notice": "Minimum Notice",
"offset_toggle": "Offset start times",
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
"offset_start": "Offset by",
"offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}",
"slot_interval": "Time-slot intervals",
"slot_interval_default": "Use event length (default)",
"delete_event_type": "Delete event type?",
@@ -1192,6 +1199,7 @@
"create_workflow": "Create a workflow",
"do_this": "Do this",
"turn_off": "Turn off",
"turn_on": "Turn on",
"settings_updated_successfully": "Settings updated successfully",
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
@@ -1623,6 +1631,7 @@
"email_user_cta": "View Invitation",
"email_no_user_invite_heading": "You’ve been invited to join a team on {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_user_invite_subheading": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
"email_no_user_step_one": "Choose your username",
"email_no_user_step_two": "Connect your calendar account",
@@ -1736,7 +1745,7 @@
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
"locked_by_admin": "Locked by admin",
"locked_by_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
@@ -1810,6 +1819,8 @@
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
"book_my_cal": "Book my Cal",
"form_updated_successfully":"Form updated successfully.",
"email_not_cal_member_cta": "Join your team",
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
"disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.",
"disable_host_confirmation_emails": "Disable default confirmation emails for host",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas pero no podrán editar ninguna configuración de la aplicación",
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
"locked_by_admin": "Bloqueado por el administrador",
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
"connect_now": "Conectar ahora",
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
@@ -237,6 +237,8 @@
"set_availability": "Définissez vos disponibilités",
"continue_without_calendar": "Continuer sans calendrier",
"connect_your_calendar": "Connectez votre calendrier",
"connect_your_video_app": "Connectez vos applications vidéo",
"connect_your_video_app_instructions": "Connectez vos applications vidéo pour les utiliser sur vos types d'événements.",
"connect_your_calendar_instructions": "Connectez votre calendrier pour vérifier automatiquement vos disponibilités ainsi que les nouveaux événements lorsqu'ils sont planifiés.",
"set_up_later": "Configurer plus tard",
"current_time": "Heure actuelle",
@@ -367,6 +369,7 @@
"create_webhook": "Créer un Webhook",
"booking_cancelled": "Réservation annulée",
"booking_rescheduled": "Réservation replanifiée",
"recording_ready": "Lien de téléchargement d'enregistrement prêt",
"booking_created": "Réservation créée",
"meeting_ended": "Rendez-vous terminé",
"form_submitted": "Formulaire envoyé",
@@ -1192,6 +1195,7 @@
"create_workflow": "Créer un workflow",
"do_this": "Effectuer ceci",
"turn_off": "Désactiver",
"turn_on": "Activer",
"settings_updated_successfully": "Paramètres mis à jour avec succès",
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"personal_cal_url": "Mon lien {{appName}} personnel",
@@ -1623,6 +1627,7 @@
"email_user_cta": "Voir l'invitation",
"email_no_user_invite_heading": "Vous avez été invité(e) à rejoindre une équipe sur {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} a été invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des rendez-vous sans les allers-retours par e-mail.",
"email_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des réunions sans les allers-retours par e-mail.",
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre équipe en un rien de temps.",
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
"email_no_user_step_two": "Connectez votre compte de calendrier",
@@ -1736,7 +1741,7 @@
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_by_admin": "Verrouillé par l'administrateur",
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
"connect_now": "Connecter maintenant",
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
@@ -1805,6 +1810,7 @@
"inline_embed": "Intégration en ligne",
"load_inline_content": "Charge votre type d'événement directement en ligne avec le contenu de votre site web.",
"floating_pop_up_button": "Bouton pop-up flottant",
"email_not_cal_member_cta": "Rejoignez votre équipe",
"disable_attendees_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour les participants",
"disable_attendees_confirmation_emails_description": "Au moins un workflow est actif sur ce type d'événement qui envoie un e-mail aux participants lorsque l'événement est réservé.",
"disable_host_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour l'hôte",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "I membri saranno in grado di vedere le app attive, ma non saranno in grado di modificare le impostazioni delle app",
"locked_webhooks_description": "I membri saranno in grado di vedere i webhook attivi, ma non saranno in grado di modificare le impostazioni dei webhook",
"locked_workflows_description": "I membri saranno in grado di vedere i flussi di lavoro attivi, ma non saranno in grado di modificare le impostazioni dei flussi di lavoro",
"locked_by_admin": "Bloccato dall'amministratore",
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
"connect_now": "Connetti ora",
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
"locked_by_admin": "管理者によりロックされています",
"app_not_connected": "{{appName}} のアカウントに接続していません。",
"connect_now": "今すぐ接続",
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
+24 -2
View File
@@ -237,6 +237,7 @@
"set_availability": "언제 여유가 있는지 설정하세요.",
"continue_without_calendar": "캘린더 없이 계속하기",
"connect_your_calendar": "캘린더 연결하기",
"connect_your_video_app_instructions": "이벤트 타입에서 사용하기 위한 비디오 앱을 연결해주세요.",
"connect_your_calendar_instructions": "바쁜 시간과 새 이벤트 일정을 자동으로 확인할 수 있도록 캘린더 연결",
"set_up_later": "나중에 하기",
"current_time": "현재 시각",
@@ -465,6 +466,7 @@
"friday": "금요일",
"saturday": "토요일",
"sunday": "일요일",
"all_booked_today": "모두 예약됨.",
"slots_load_fail": "사용 가능한 시간 슬롯을 로드할 수 없습니다.",
"additional_guests": "게스트 추가",
"your_name": "당신의 이름",
@@ -1038,6 +1040,7 @@
"event_cancelled_trigger": "이벤트 취소 시",
"new_event_trigger": "새 이벤트 예약 시",
"email_host_action": "호스트에게 이메일 발송",
"email_attendee_action": "참석자들에게 이메일 전송",
"sms_attendee_action": "참석자에게 SMS 발송",
"sms_number_action": "특정 번호로 SMS 발송",
"workflows": "워크플로",
@@ -1246,6 +1249,7 @@
"calendars_description": "이벤트 타입이 캘린더와 상호 작용하는 방식 구성",
"appearance_description": "예약 모양 설정 관리",
"conferencing_description": "회의에 즐겨 사용하는 화상 회의 앱 추가",
"add_conferencing_app": "회의 앱 추가",
"password_description": "계정 비밀번호 설정 관리",
"2fa_description": "계정 비밀번호 설정 관리",
"we_just_need_basic_info": "프로필 설정을 위해 몇 가지 기본 정보만 있으면 됩니다.",
@@ -1620,6 +1624,7 @@
"email_user_cta": "초대 보기",
"email_no_user_invite_heading": "{{appName}} 팀에 초대되었습니다",
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
"email_user_invite_subheading": "{{invitedBy}}님이 {{appName}}에서 자신의 `{{teamName}}` 팀에 가입하도록 당신을 초대했습니다. {{appName}}은 유저와 팀이 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 하는 이벤트 조율 스케줄러입니다.",
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 팀과 함께 스트레스 없이 편리하게 일정을 관리할 수 있게 될 것입니다.",
"email_no_user_step_one": "사용자 이름 선택",
"email_no_user_step_two": "캘린더 계정 연결",
@@ -1695,6 +1700,14 @@
"spot_popular_event_types_description": "어떤 이벤트 타입이 가장 많은 클릭을 받고 예약이 되고 있는지 확인해보세요",
"no_responses_yet": "아직 응답 없음",
"this_will_be_the_placeholder": "이것은 자리 표시자가 됩니다",
"error_booking_event": "이벤트를 예약하는 도중 오류가 발생했습니다. 페이지를 새로고침한 후 다시 시도해주세요.",
"timeslot_missing_title": "시간대가 선택되지 않았습니다",
"timeslot_missing_description": "이벤트를 예약 할 시간대를 선택해주세요.",
"timeslot_missing_cta": "시간대 선택",
"switch_monthly": "월별 보기로 전환",
"switch_weekly": "주간 보기로 전환",
"switch_multiday": "일간 보기로 전환",
"num_locations": "{{num}}개의 위치 옵션",
"this_meeting_has_not_started_yet": "이 회의는 아직 시작되지 않았습니다",
"this_app_requires_connected_account": "{{appName}}에 연결된 {{dependencyName}} 계정이 필요합니다",
"connect_app": "{{dependencyName}} 연결",
@@ -1724,7 +1737,6 @@
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
"locked_by_admin": "관리자에 의해 잠금 됨",
"app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.",
"connect_now": "지금 연결",
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
@@ -1782,5 +1794,15 @@
"seats_and_no_show_fee_error": "현재 좌석을 활성화할 수 없으므로 노쇼 수수료를 부과할 수 없습니다",
"complete_your_booking": "예약을 완료하세요",
"complete_your_booking_subject": "예약 완료: {{title}} 날짜 {{date}}",
"email_invite_team": "{{email}} 이 초대되었습니다."
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "참석자에게 {{amount, currency}}을 청구하려고 합니다. 계속 진행하시겠습니까?",
"charge_attendee": "참석자에게 {{amount, currency}}을 청구합니다.",
"payment_app_commission": "결제가 필요합니다 (건당 수수료 {{paymentFeePercentage}}% + {{fee, currency}})",
"email_invite_team": "{{email}} 이 초대되었습니다.",
"image_size_limit_exceed": "업로드되는 이미지의 크기는 5MB를 초과하면 안 됩니다.",
"need_help_embedding": "도움이 필요하신가요? Wix, Squarespace, 또는 WordPress에서 Cal을 임베드하는 가이드를 참조하거나, 자주 묻는 질문을 확인하거나, 고급 임베드 옵션을 살펴보세요.",
"disable_attendees_confirmation_emails": "참석자에 대한 기본 확인 이메일 비활성화",
"disable_attendees_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약되었을 때 참석자들에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다.",
"disable_host_confirmation_emails": "호스트에 대한 기본 확인 이메일 비활성화",
"disable_host_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약될 때 호스트에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다."
}
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
"locked_by_admin": "Vergrendeld door beheerder",
"app_not_connected": "U heeft geen {{appName}}-account gekoppeld.",
"connect_now": "Nu koppelen",
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Członkowie będą mogli zobaczyć aktywne aplikacje, ale nie będą mogli zmieniać ich ustawień.",
"locked_webhooks_description": "Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli zmieniać ich ustawień.",
"locked_workflows_description": "Członkowie będą mogli zobaczyć aktywne przepływy pracy, ale nie będą mogli zmieniać ich ustawień.",
"locked_by_admin": "Zablokowane przez administratora",
"app_not_connected": "Nie połączono konta aplikacji {{appName}}.",
"connect_now": "Połącz teraz",
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
"locked_by_admin": "Bloqueado pelo administrador",
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
"connect_now": "Conectar agora",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
"locked_by_admin": "Bloqueado pelo administrador",
"app_not_connected": "Não associou uma conta {{appName}}.",
"connect_now": "Associar agora",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
"locked_by_admin": "Blocat de administrator",
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
"connect_now": "Conectați-l acum",
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
"locked_by_admin": "Заблокировано администратором",
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
"connect_now": "Подключить сейчас",
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju bilo kakva podešavanja aplikacije",
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju bilo kakva podešavanja webhook-ova",
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju bilo kakva podešavanja radnih tokova",
"locked_by_admin": "Zaključao administrator",
"app_not_connected": "Niste povezali {{appName}} nalog.",
"connect_now": "Povežite odmah",
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Medlemmarna kommer att kunna se de aktiva apparna, men de kommer inte att kunna ändra appinställningarna",
"locked_webhooks_description": "Medlemmarna kommer att kunna se aktiva webhooks men inte kunna redigera inställningar för webhooks",
"locked_workflows_description": "Medlemmarna kommer att kunna se de aktiva arbetsflödena, men de kommer inte att kunna ändra några arbetsflödesinställningar",
"locked_by_admin": "Låst av administratör",
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
"connect_now": "Anslut nu",
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
@@ -1,4 +1,5 @@
{
"identity_provider": "அடையாள வழங்குபவர்",
"trial_days_left": "உங்கள் மேம்படுத்தப்பட்ட பயன்பாட்டு சோதனையில் $t(day, {\"count\": {{days}} }) மீதமுள்ளது",
"day_one": "{{count}} நாள்",
"day_other": "{{count}} நாட்கள்",
@@ -7,6 +8,7 @@
"upgrade_now": "மேம்படுத்தவும்",
"accept_invitation": "அழைப்பை ஏற்கவும்",
"calcom_explained": "{{appName}} அனைவருக்கும் திட்டமிடல் உள்கட்டமைப்பை வழங்குகிறது.",
"calcom_explained_new_user": "உங்கள் {{appName}} கணக்கை அமைக்கவும்! உங்கள் திட்டமிடல் சிக்கல்கள் அனைத்தையும் தீர்க்க சில படிகள் மட்டுமே உள்ளன.",
"have_any_questions": "கேள்விகள் உள்ளதா? நாங்கள் உதவ தயாராக இருக்கிறோம்.",
"reset_password_subject": "{{appName}}: கடவுச்சொல் மீட்டமைக்கும் வழிமுறைகள்",
"event_declined_subject": "{{title}} {{date}} அன்று நிராகரிக்கப்பட்டது",
@@ -28,8 +30,58 @@
"your_event_has_been_scheduled": "உங்கள் நிகழ்வு திட்டமிடப்பட்டது",
"your_event_has_been_scheduled_recurring": "உங்கள் தொடர் நிகழ்வு திட்டமிடப்பட்டுள்ளது",
"accept_our_license": ".env மாறியை மாற்றுவதன் மூலம் எங்கள் உரிமத்தை ஏற்கவும்\n<1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
"error_message": "பிழை செய்தி: '{{errorMessage}}'",
"refund_failed_subject": "பணத்தைத் திரும்பப் பெற முடியவில்லை: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "{{date}} அன்று {{userName}} உடன் நடந்த {{eventType}} நிகழ்விற்கான பணத்தைத் திரும்பி அனுப்ப இயலவில்லை.",
"check_with_provider_and_user": "உங்கள் கட்டண வழங்குநர் மற்றும் {{user}} உடன் இனைந்து இதை எவ்வாறு கையாள்வது என்பதைச் சரிபார்க்கவும்.",
"a_refund_failed": "பணத்தைத் திரும்பப் பெற முடியவில்லை",
"awaiting_payment_subject": "பணம் செலுத்த காத்திருக்கிறது: {{date}} அன்று {{title}}",
"meeting_awaiting_payment": "உங்கள் மீட்டிங் கட்டணம் செலுத்த காத்திருக்கிறது",
"help": "உதவி",
"price": "விலை",
"paid": "பணம் செலுத்தப்பட்டது",
"refunded": "பணம் திருப்பிக் கொடுக்கப்பட்டது",
"payment": "பணம் செலுத்துதல்",
"missing_card_fields": "கார்டு புலங்கள் இல்லை",
"pay_now": "இப்போது பணம் செலுத்த",
"codebase_has_to_stay_opensource": "கோட்பேஸ் மாற்றப்பட்டாலும் இல்லாவிட்டாலும் திறந்த மூலமாக இருக்க வேண்டும்",
"cannot_repackage_codebase": "நீங்கள் கோட்பேஸை மீண்டும் பேக்கேஜ் செய்யவோ விற்கவோ முடியாது",
"acquire_license": "மின்னஞ்சல்மூலம் இந்த விதிமுறைகளை அகற்ற வணிக உரிமத்தைப் பெறவும்",
"terms_summary": "விதிமுறைகளின் சுருக்கம்",
"open_env": ".env ஐத் திறந்து எங்கள் உரிமத்தை ஏற்கவும்",
"env_changed": "நான் என் .env ஐ மாற்றிவிட்டேன்",
"accept_license": "உரிமத்தை ஏற்கவும்",
"still_waiting_for_approval": "ஒரு நிகழ்வு இன்னும் ஒப்புதலுக்காகக் காத்திருக்கிறது",
"event_is_still_waiting": "நிகழ்வு கோரிக்கை இன்னும் காத்திருக்கிறது: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "மேலும் முடிவுகள் இல்லை",
"no_results": "முடிவுகள் இல்லை",
"load_more_results": "மேலும் முடிவுகளை ஏற்றவும்",
"integration_meeting_id": "{{integrationName}} சந்திப்பு ஐடி: {{meetingId}}",
"confirmed_event_type_subject": "உறுதிப்படுத்தப்பட்டது: {{date}} அன்று {{name}} உடன் {{eventType}}",
"new_event_request": "புதிய நிகழ்வு கோரிக்கை: {{attendeeName}} - {{date}} - {{eventType}}",
"confirm_or_reject_request": "கோரிக்கையை உறுதிப்படுத்தவும் அல்லது நிராகரிக்கவும்",
"check_bookings_page_to_confirm_or_reject": "முன்பதிவை உறுதிப்படுத்த அல்லது நிராகரிக்க உங்கள் முன்பதிவு பக்கத்தைப் பார்க்கவும்.",
"event_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு காத்திருக்கிறது",
"event_awaiting_approval_recurring": "தொடர்ச்சியான நிகழ்வு உங்கள் ஒப்புதலுக்காகக் காத்திருக்கிறது",
"someone_requested_an_event": "உங்கள் காலெண்டரில் ஒரு நிகழ்வைத் திட்டமிடுமாறு ஒருவர் கோரியுள்ளார்.",
"someone_requested_password_reset": "உங்கள் கடவுச்சொல்லை மாற்ற யாரோ ஒரு இணைப்பைக் கோரியுள்ளனர்.",
"password_reset_instructions": "நீங்கள் இதைக் கோரவில்லை எனில், இந்த மின்னஞ்சலைப் பாதுகாப்பாகப் புறக்கணிக்கலாம், மேலும் உங்கள் கடவுச்சொல் மாற்றப்படாது.",
"event_awaiting_approval_subject": "ஒப்புதலுக்காகக் காத்திருக்கிறது: {{title}} {{date}} இல்",
"event_still_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு இன்னும் காத்திருக்கிறது",
"booking_submitted_subject": "முன்பதிவு சமர்ப்பிக்கப்பட்டது: {{title}} {{date}} அன்று",
"download_your_recording": "உங்கள் பதிவைப் பதிவிறக்கவும்",
"your_meeting_has_been_booked": "உங்கள் சந்திப்பு பதிவு செய்யப்பட்டுள்ளது",
"event_type_has_been_rescheduled_on_time_date": "உங்கள் {{title}}, {{date}} க்கு மாற்றியமைக்கப்பட்டுள்ளது.",
"event_has_been_rescheduled": "புதுப்பிக்கப்பட்டது - உங்கள் நிகழ்வு மீண்டும் திட்டமிடப்பட்டது",
"request_reschedule_title_attendee": "உங்கள் முன்பதிவை மாற்றியமைக்க கோரிக்கை",
"request_reschedule_subtitle": "{{organizer}} முன்பதிவை ரத்துசெய்துவிட்டு, மற்றொரு நேரத்தைத் தேர்ந்தெடுக்கும்படி கோரியுள்ளார்.",
"request_reschedule_title_organizer": "நீங்கள் {{attendee}} ஐ மறுஅட்டவணைக்குக் கோரியுள்ளீர்கள்",
"request_reschedule_subtitle_organizer": "நீங்கள் முன்பதிவை ரத்து செய்துவிட்டீர்கள், {{attendee}} உங்களுடன் புதிய முன்பதிவு நேரத்தைத் தேர்ந்தெடுக்க வேண்டும்.",
"rescheduled_event_type_subject": "மறுஅட்டவணைக்கான கோரிக்கை அனுப்பப்பட்டது: {{eventType}} உடன் {{name}} உடன் {{date}}",
"requested_to_reschedule_subject_attendee": "மறுஅட்டவணை செயல் தேவை: {{eventType}} க்கு {{name}} உடன் புதிய நேரத்தை முன்பதிவு செய்யவும்",
"reschedule_reason": "மறு அட்டவணைக்கான காரணம்",
"hi_user_name": "வணக்கம் {{name}}",
"turn_on": "இயக்கவும்",
"email_no_user_invite_heading": "நீங்கள் {{appName}} குழுவில் சேர அழைக்கப்பட்டுள்ளீர்கள்",
"email_no_user_invite_subheading": "{{invitedBy}} உங்களை {{appName}} குழுவில் சேர அழைத்துள்ளார். {{appName}} என்பது ஈமெயில் இல்லாமலேயே உங்களையும் உங்கள் குழுவையும் கூட்டங்களைத் திட்டமிடுவதற்கு உதவும் திட்டமிடல் பயன்பாடு.",
"email_no_user_invite_steps_intro": "சில சிறிய முயர்ச்சித்தல் மூலம் நாங்கள் உங்களை அழைத்துச் செல்வோம், எந்த நேரத்திலும் உங்கள் குழுவுடன் மன அழுத்தமில்லாத திட்டமிடலை அனுபவிப்பீர்கள்.",
@@ -114,6 +166,7 @@
"select_all": "அனைத்தையும் தேர்வுசெய்",
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
"locked_by_admin": "குழு நிர்வாகியால் பூட்டப்பட்டது",
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
"select_user": "பயனரைத் தேர்ந்தெடுக்கவும்",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
"locked_by_admin": "Yönetici tarafından kilitlendi",
"app_not_connected": "Bir {{appName}} hesabı bağlamadınız.",
"connect_now": "Hemen bağlan",
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка",
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
"locked_by_admin": "Заблоковано адміністратором",
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
"connect_now": "Під’єднати",
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
"locked_by_admin": "Bị khoá bởi quản trị viên",
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
"connect_now": "Kết nối ngay",
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
@@ -1191,6 +1191,7 @@
"create_workflow": "创建工作流程",
"do_this": "执行此操作",
"turn_off": "关闭",
"turn_on": "打开",
"settings_updated_successfully": "设置已成功更新",
"error_updating_settings": "更新设置时出错",
"personal_cal_url": "我的个人 {{appName}} URL",
@@ -1726,7 +1727,7 @@
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
"locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置",
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
"locked_by_admin": "被管理员锁定",
"locked_by_admin": "被团队管理员锁定",
"app_not_connected": "您尚未连接 {{appName}} 账户。",
"connect_now": "立即连接",
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
@@ -1724,7 +1724,6 @@
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
"locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定",
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
"locked_by_admin": "已由管理員鎖定",
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
"connect_now": "立即連結",
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
+68 -4
View File
@@ -151,10 +151,6 @@ const TestData = {
},
};
const ctx = {
prisma: prismaMock,
};
type App = {
slug: string;
dirName: string;
@@ -186,6 +182,7 @@ type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
users?: { id: number }[];
@@ -711,6 +708,72 @@ describe("getSchedule", () => {
);
});
test("Start times are offset (offsetStart)", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scenarioData = {
eventTypes: [
{
id: 1,
length: 25,
offsetStart: 5,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
createBookingScenario(scenarioData);
const schedule = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(schedule).toHaveTimeSlots(
[
`04:05:00.000Z`,
`04:35:00.000Z`,
`05:05:00.000Z`,
`05:35:00.000Z`,
`06:05:00.000Z`,
`06:35:00.000Z`,
`07:05:00.000Z`,
`07:35:00.000Z`,
`08:05:00.000Z`,
`08:35:00.000Z`,
`09:05:00.000Z`,
`09:35:00.000Z`,
`10:05:00.000Z`,
`10:35:00.000Z`,
`11:05:00.000Z`,
`11:35:00.000Z`,
`12:05:00.000Z`,
],
{
dateString: plus2DateString,
}
);
});
test("Check for Date overrides", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@@ -1132,6 +1195,7 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
@@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import { buildEventType } from "@calcom/lib/test/builder";
import type { CompleteEventType } from "@calcom/prisma/zod";
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
import { prismaMock } from "../../../../tests/config/singleton";
@@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => {
expect(result.deletedExistentEventTypes).toEqual([123]);
});
});
describe("Workflows", () => {
it("Links workflows to new and existing assigned members", async () => {
const { schedulingType, id, teamId, locations, timeZone, parentId, userId, ...evType } =
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
workflows: [
{
workflowId: 11,
} as CompleteWorkflowsOnEventTypes,
],
});
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
locations: [],
parentId: 1,
userId: 5,
users: {
connect: [
{
id: 5,
},
],
},
workflows: {
create: [{ workflowId: 11 }],
},
},
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
workflows: undefined,
scheduleId: undefined,
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
create: {
eventTypeId: 2,
workflowId: 11,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: 2,
workflowId: 11,
},
},
});
});
});
});
+27
View File
@@ -24,6 +24,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(24);
@@ -46,6 +47,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(12);
@@ -66,6 +68,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
@@ -87,6 +90,7 @@ describe("Tests the slot logic", () => {
minimumBookingNotice: 0,
workingHours,
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
@@ -108,6 +112,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
@@ -129,8 +134,30 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 20,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(71);
});
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 25,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 25,
offsetStart: 5,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(48);
});
});
+7 -2
View File
@@ -23,8 +23,12 @@ for module in "$@"; do
git submodule add --force $project "apps/$module"
# Set the default branch to main
git config -f .gitmodules --add "submodule.apps/$module.branch" main
# Adding the subdmoule ignores the `.gitignore` so a reset is needed
git reset
# Update to the latest from main in that submodule
cd apps/$module && git pull origin main && cd ../..
# We forcefully added the subdmoule which was in .gitignore, so unstage it.
git restore --staged apps/$module
else
# If the module is the API, display a link to request access
if [ "$module" = "api" ]; then
@@ -35,3 +39,4 @@ for module in "$@"; do
fi
fi
done
git restore --staged .gitmodules
+3 -1
View File
@@ -17,6 +17,7 @@
"app-store": "yarn app-store-cli cli",
"create-app": "yarn app-store create",
"edit-app": "yarn app-store edit",
"publish-embed": "yarn workspaces foreach --from=\"@calcom/embed*\" npm publish --access=public",
"delete-app": "yarn app-store delete",
"create-app-template": "yarn app-store create-template",
"edit-app-template": "yarn app-store edit-template",
@@ -74,6 +75,7 @@
"web": "yarn workspace @calcom/web"
},
"devDependencies": {
"@changesets/cli": "^2.26.1",
"@deploysentinel/playwright": "^0.3.3",
"@playwright/test": "^1.31.2",
"@snaplet/copycat": "^0.3.0",
@@ -95,7 +97,7 @@
"dependencies": {
"city-timezones": "^1.2.1",
"eslint": "^8.34.0",
"lucide-react": "^0.130.0",
"lucide-react": "^0.171.0",
"turbo": "^1.8.3"
},
"resolutions": {
+1
View File
@@ -1,5 +1,6 @@
{
"name": "@calcom/app-store-cli",
"private": true,
"sideEffects": false,
"version": "0.0.0",
"bin": "dist/cli.js",
@@ -4,9 +4,9 @@ import SelectInput from "ink-select-input";
import TextInput from "ink-text-input";
import React, { useEffect, useState } from "react";
import { AppMeta } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { getSlugFromAppName, BaseAppFork, Seed, generateAppFiles, getAppDirPath } from "../core";
import { getSlugFromAppName, BaseAppFork, generateAppFiles, getAppDirPath } from "../core";
import { getApp } from "../utils/getApp";
import Templates from "../utils/templates";
import Label from "./Label";
@@ -148,8 +148,6 @@ export const AppForm = ({
oldSlug: givenSlug,
});
await Seed.update({ slug, category: category, oldSlug: givenSlug, isTemplate });
await generateAppFiles();
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
@@ -242,6 +240,10 @@ export const AppForm = ({
<Text color="green">Publisher Email: </Text>
<Text>{email}</Text>
</Box>
<Text bold>
Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email:
admin@example.com, Pass: ADMINadmin2022!)
</Text>
</Box>
</Box>
)}
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
import { ImportantText } from "../components/ImportantText";
import { Message } from "../components/Message";
import { BaseAppFork, Seed, generateAppFiles } from "../core";
import { BaseAppFork, generateAppFiles } from "../core";
import { getApp } from "../utils/getApp";
export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) {
@@ -28,7 +28,6 @@ export default function DeleteForm({ slug, action }: { slug: string; action: "de
if (state === "DELETION_CONFIRMATION_SUCCESSFUL") {
(async () => {
await BaseAppFork.delete({ slug, isTemplate });
Seed.revert({ slug });
await generateAppFiles();
// successMsg({ text: `App with slug ${slug} has been deleted`, done: true });
setState("DELETION_COMPLETED");
-58
View File
@@ -1,8 +1,6 @@
import fs from "fs";
import path from "path";
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
import execSync from "./utils/execSync";
@@ -27,10 +25,6 @@ export function getAppDirPath(slug: string, isTemplate: boolean) {
return path.join(TEMPLATES_PATH, `${slug}`);
}
function absolutePath(appRelativePath: string) {
return path.join(APP_STORE_PATH, appRelativePath);
}
const updatePackageJson = ({
slug,
appDescription,
@@ -138,58 +132,6 @@ export const BaseAppFork = {
},
};
export const Seed = {
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
update: async function ({
slug,
category,
oldSlug,
isTemplate,
}: {
slug: string;
category: string;
oldSlug: string;
isTemplate: boolean;
}) {
let configContent = "[]";
try {
if (fs.statSync(this.seedConfigPath)) {
configContent = fs.readFileSync(this.seedConfigPath).toString();
}
} catch (e) {}
let seedConfig: typeof seedAppStoreConfig = JSON.parse(configContent);
seedConfig = seedConfig.filter((app) => app.slug !== oldSlug);
if (!seedConfig.find((app) => app.slug === slug)) {
seedConfig.push({
dirName: slug,
categories: [category],
slug: slug,
type: `${slug}_${category}`,
isTemplate: isTemplate,
});
}
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
await execSync(`cd ${workspaceDir}/packages/prisma && yarn seed-app-store seed-templates`);
},
revert: async function ({ slug }: { slug: string }) {
let seedConfig: typeof seedAppStoreConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
seedConfig = seedConfig.filter((app) => app.slug !== slug);
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
await execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
},
};
export const generateAppFiles = async () => {
await execSync(`yarn ts-node --transpile-only src/build.ts`);
};
+16 -2
View File
@@ -5,8 +5,22 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
import type { AppFrontendPayload as App } from "@calcom/types/App";
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
export async function getAppWithMetadata(app: { dirName: string }) {
const appMetadata: App | null = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
/**
* Get App metdata either using dirName or slug
*/
export async function getAppWithMetadata(app: { dirName: string } | { slug: string }) {
let appMetadata: App | null;
if ("dirName" in app) {
appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
} else {
const foundEntry = Object.entries(appStoreMetadata).find(([, meta]) => {
return meta.slug === app.slug;
});
if (!foundEntry) return null;
appMetadata = foundEntry[1] as App;
}
if (!appMetadata) return null;
// Let's not leak api keys to the front end
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -47,6 +47,7 @@ export default function OmniInstallAppButton({
<InstallAppButton
type={app.type}
isProOnly={app.isProOnly}
teamsPlanRequired={app.teamsPlanRequired}
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
+8 -1
View File
@@ -12,7 +12,14 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
if (calendarType?.endsWith("_other_calendar")) {
calendarType = calendarType.split("_other_calendar")[0];
}
const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore]();
const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore];
if (!calendarAppImportFn) {
log.warn(`calendar of type ${calendarType} is not implemented`);
return null;
}
const calendarApp = await calendarAppImportFn();
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
log.warn(`calendar of type ${calendarType} is not implemented`);
return null;

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