Compare commits

...
192 Commits
Author SHA1 Message Date
Omar López 9bc40a3eb6 v3.3.0
release.yml / release (push) Canceled after 0s
2023-09-14 12:50:43 -07:00
Keith Williamsandzomars bd21b2dfe1 fix: Import prisma (#11369)
Co-authored-by: zomars <zomars@me.com>
2023-09-14 19:47:25 +00:00
Crowdin Bot d60fb081c9 New Crowdin translations by Github Action 2023-09-14 16:56:47 +00:00
197b435b6f fix: Duplicate calendar events (#11327)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-09-14 09:53:58 -07:00
dihedral add297b09a fix: use selected duration when checking for conflicts for new bookings (#10713) 2023-09-14 10:40:49 -04:00
Ujwal Kumar d65f0ee0e8 fix: Allow attendee first and last name from attendee name when name … (#11287) 2023-09-14 10:24:58 -04:00
Hariom Balhara 59c0a71db7 fix: Routing Forms - Forward Query Params and show the error message in case of validation error in headless router (#11338) 2023-09-14 10:05:22 -04:00
Hariom Balhara d50151fe08 Fix yarn.lock (#11364) 2023-09-14 13:41:56 +00:00
Kiran K d2b0dd8eab fix: SAML SSO - Stop relying on invitedTo column and use membership table instead (#11331) 2023-09-14 14:57:45 +02:00
Joe Au-Yeung bb0574dff8 chore: Outlook processBusyTimes iterate busy times once (#11350) 2023-09-14 14:56:31 +02:00
sean-brydon 49364eee9a Import prisma (#11358) 2023-09-14 09:18:19 -03:00
Peer Richelsen adb0530d69 feat: getalby.com app (#11347) 2023-09-14 08:35:13 -03:00
+18 a2d1dbebba feat: editmode availbility slider (#11197)
* Add loading data and banner

* [WIP] hasEditPerms middleware

* fix: type error in booker (#11011)

* New Crowdin translations by Github Action

* refactor: removed redundant test (#10785)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>

* feat: 2fa backup codes (#10600)

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

* Delete add-PRs-to-project-reviewing-PRs.yml (#11008)

Co-authored-by: alannnc <alannnc@gmail.com>

* New Crowdin translations by Github Action

* fix: multiple duration when booking (#11032)

* fix: other reported issues (#11015)

* fix: weird margin top in avatar

* fix: pending users are shown on booking page

* fix: avatar and naming issues

* fix: toast alignment and removing unneeded titles

* missing changes from toast improvements

* feat: empty state for teams without event types

* Removing console.log

* feat: cal ai (#10992)

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* New Crowdin translations by Github Action

* fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)

Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* feat: team admin: see connected apps of team members (#11036)

* added feature:team admin can see connected apps of members

* fixed the type error

* Update packages/lib/server/queries/teams/index.ts

* Minor fixes

---------

Co-authored-by: alannnc <alannnc@gmail.com>

* fix: lower case slugs in teams (#11026)

* fix: lower case slugs in teams

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

* fix: use slugify

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

---------

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

* refactor: improvements on german translation (#10898)

* fix: fix-tablet-menu-not-centered-sidebar (#11020)

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

* chore: add Popover in storybook (#11021)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: Set mobile availability (#11027)

* chore: add ColorPicker in storybook (CALCOM-10760) (#10866)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: adds next cold start profiler (#11014)

* Handle disabling of ORGANIZATIONS_ENABLED flag (#11041)

* New Crowdin translations by Github Action

* styles:dark mode color fix (#11004)

* chore: sheet darkmode and improve responsive (#11047)

* fix: handle collective multiple host on destinationCalendar (#10967)

* fix: include app data and credentials from DB (#11048)

* include app data and credentials from DB

* Improve performance

* fix: Error when running storybook (#11037)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: broken company profile link on team booking page (#10978)

* chore: add ErrorBoundary in storybook (CALCOM-10760) (#10872)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* feat: sorting for workflow and routing forms (#10780)

Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* resolve zod versions across child packages (#11052)

* fix: List storybook file is empty (fix-list) (#10965)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: admin org list without members (#11051)

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

* chore: add ToggleGroup in storybook (#10802)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* fix: add metadata to stripe payment intent (#11053)

* fix: Logo storybook file with invalid icons (fix-logo) (#11018)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Add controls for Select Field storybook file (#10936)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: Fix tooltip control on ButtonPlayground storybook file (fix-ButtonTooltip) (#10937)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>

* fix: email embed – remove collapsible and permanently show times (#10996)

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Mehul <mehulzr@gmail.com>

* chore: add Timezone Select in storybook (CALCOM-10760) (#10966)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* chore: add Switch in storybook (CALCOM-10760) (#10804)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Sync packages

* fix: zod utils due to upgrade

* easy fix (#11054)

* fix sub quantity update stripe (#11057)

* v3.2.7

* Revert "feat: adds next cold start profiler (#11014)" (#11072)

This reverts commit 05631d093f.

* fix: Fixes username invite issue (#10998)

* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

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

* Fix sheet layout

* WIP permissions when userschedule doesnt match

* WIP get handler weird behaviour

* Update toast

* add disabled - handle membership overlap

* Handle permissions + perf improvments

* use input uid

* Remove Console.log

* Clean up

* Revert changes accidental

* Fix merge artifacts

* Remove dead code

* Remove code after return

* Update read permission check

* Revert avatar changes as fixed elsewhere

* Handle if user has not completed onboarding

* Disable button

* Update packages/lib/hasEditPermissionForUser.ts

* Correct Error throwing

* Update packages/features/timezone-buddy/components/AvailabilityEditSheet.tsx

* Fix type erro

* Add i18n

* Improve Spacing

* Update yarn.lock

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: DexterStorey <36115192+DexterStorey@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Monto <138862352+montocoder@users.noreply.github.com>
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Abhijeet Singh <asingh9829@gmail.com>
Co-authored-by: Kamil B. Demirci <kamil.demirci@indyaner.ch>
Co-authored-by: Denzil Samuel <71846487+samueldenzil@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Sahil Padvi <71093044+Sahil25061999@users.noreply.github.com>
Co-authored-by: Patel Divyesh <pateldivyesh1323@gmail.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: Mehul <mehulzr@gmail.com>
2023-09-14 09:45:13 +01:00
alannnc 605e744a58 fix: remove avatar form user select when loading insights (#11353) 2023-09-14 09:37:41 +02:00
alannnc aab0d6b18a fix: paypal save event type and payment (#11181)
* fix-paypal-save

* change type to use appId instead

* Remove duplicated code
2023-09-14 00:29:03 -07:00
WesleyandHariom Balhara dfde17111c test: verify embed helpers (#11318)
* test: add test cases to `window.getEmbedNamespace`

* test: add test cases when window.name doesn't have cal-embed=

* test: add test case to `getembedTheme` returning the theme from embedStore

* test: add test to `getEmbedTheme` returning the theme from query params

* Small refactor to reduce the time taken by tests and fix missing typings for embed functions on window

* Fix weird test failure

* Fix type import error caught by api.test.test

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-14 07:14:54 +00:00
Hariom Balhara 2591ad12df chore: Enable prettier on vitest files without causing test failures (#11190) 2023-09-14 08:58:08 +02:00
Kevin Crimiandalannnc 3a05ae6f74 feat: Add more metadata to Stripe transaction (#11221)
* Add booker info, event type title, and organizer info into Stripe metadata

* Add back bookingTitle in addition to eventTitle

* Remove .tool-versions

* Fix typo

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-09-14 04:10:42 +00:00
efcfd7bb9b fix: when creating new user, set email handle as username (#11238)
* When creating new user, set email handle as username

* Accounting for external invited users

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-13 20:46:41 -03:00
Leo Giovanetti bce62829b7 fix: org shell and org user settings avatars + org owner username (#11342) 2023-09-13 18:04:36 +00:00
Carina WollendorferandCarinaWolli f320e486a2 fix issue with duplicate children event types (#11337)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-13 17:04:14 +00:00
MeenuyD 8c96e04b51 fix: insights upgrade screen has no title and subtitle for upgrade screen (#11328) 2023-09-13 15:41:53 +02:00
9eb3698ea5 fix: multiple organizer address bug (#11299)
* fix: multiple organizer address bug

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

* fix: add inPerson condition

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

* fix: change options

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

* Send _type along with the response to be able to determine what input was it. We can use that later

* Revert "Send _type along with the response to be able to determine what input was it. We can use that later"

This reverts commit c75ba57ad14b07d629b218d6e3eaede93b99ebaa.

* Update multiple organizer address test to verify the test fix

* Add a test to ensure that sending label would correctly add that label as location

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-13 04:19:01 +00:00
sean-brydonandPeer Richelsen 8ff07807f9 Fix overflow (#11320)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-12 21:46:18 +00:00
Crowdin Bot 9c7f655329 New Crowdin translations by Github Action 2023-09-12 21:29:30 +00:00
Alex van Andel 5c72fca816 Revert "fix: improve UX of team invite by copy link (#11009)"
This reverts commit 0150041496.
2023-09-12 22:26:22 +01:00
Alex van Andel aeafa3ef0a v3.2.10 2023-09-12 18:17:53 +01:00
Joe Au-YeungandAlex van Andel 3d4a3c7a93 fix: Connect selected calendar to credential (#11283)
release.yml / release (push) Canceled after 0s
* Reduce the unhandled promise

* Connect selected calendar to credential

* Write credential to selected calendar

* Type fix

* Handle catching promises in separate threads

* Only call to list calendars if there are no credentialIds

* Changed type of IntegrationCalendar from undef to null

* Adding missing property to getting started

* Also add calendars.tsx

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-12 18:09:05 +01:00
alannnc 1c1625d3fc fix: org admin delete other teams (#11247)
* fix org-admin delete other teams

* Fix delete from teams to delete from orgs endpoint
2023-09-12 16:19:17 +00:00
alannnc 75f9f045b8 fix: payments on round-robin events (#11248)
* Fix condition when no users are found for event type

* fix for types
2023-09-12 08:59:32 -07:00
Leo Giovanetti 86af383f6c feat: autolink org on provider signup (#11267)
* feat: autolink org on provider signup

* Adding env var in example

* slugifying inferred org username

* Updated org README

* Optional env var for autolinking added to readme

* Typo

* md syntax correction

* Verifying user when autolinked

* Also verifying users invited to org

* Applying feedback

* Scoping to Google for now

* Updating comment

* Updating readme with Google scoping
2023-09-12 15:17:34 +00:00
Keith WilliamsandAlex van Andel 84408025ed perf: pre-load booking data for all users (#11243)
* perf: pre-load booking data for all users

* Fixing property issues

* Fixes

* Removed unwanted console.log

* Made comment for intention of refactor more clear

* fix: types

* _count is optional

* Assign to const bookings

* Added mock for prisma booking

* Fixed unit tests

* Added lint ignore for prisma field

* Update bookingScenario.ts

* Fix linting

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-09-12 14:12:25 +01:00
Hariom Balhara 9fba8c548b fix: Prefill not working if firstAndLastName variant enabled in name field and name doesn't have any space (#11289) 2023-09-12 13:00:14 +02:00
sean-brydon 56f3647ec2 Fix max w (#11294) 2023-09-12 09:59:40 +00:00
GitStart-Cal.comandgitstart-calcom a5cb3ce573 feat: Create VerifyCodeDialog storybook file (#11286)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-12 00:09:13 +02:00
Udit Takkar 7581f4d641 fix: add inline color in og image (#11280) 2023-09-12 00:08:27 +02:00
Crowdin Bot a1d67b256e New Crowdin translations by Github Action 2023-09-11 18:41:56 +00:00
Joe Au-Yeung 75f9dad16e fix: Selected calendar unhandled promise when removing calendars (#11281) 2023-09-11 14:39:27 -04:00
sean-brydonandLeo Giovanetti 0a0d212373 fix: Handle removing user from org (#11227)
* Handle removing user from org

* Address no password or username being set

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-09-11 14:46:38 +00:00
b8794ba807 fix: Fixed team image not showing on event-types page (#11231)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-11 11:37:42 +00:00
Ahsan AhmadandPeer Richelsen 9b8e4ed385 fix: README typo uppercase ID (#11254)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-11 13:19:13 +02:00
Somay Chauhan 997b5d5290 fix: "no teams" text not visibl in organization's public url (#11257) 2023-09-11 13:10:36 +02:00
Yash d7080b41cb fix: text-emphasis removed (#11228) 2023-09-11 13:05:03 +02:00
Ujwal Kumar 32754bd120 fix: add hardcoded strings to locales so they can be translated (#11266) 2023-09-11 13:00:38 +02:00
Meet Mota 13fd02d174 fix: smaller date overwrite when no date is selected (#11250) 2023-09-11 12:49:27 +02:00
Crowdin Bot d6cbac54e0 New Crowdin translations by Github Action 2023-09-11 10:36:13 +00:00
Udit Takkar 489ffdf49a fix: hide book a team member (#11271) 2023-09-11 10:33:30 +00:00
Hariom BalharaandJoe Au-Yeung 37b22977ee fix: Booking and Cancellation related text changes (#10744)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-10 17:00:51 +00:00
Crowdin Bot 848a040162 New Crowdin translations by Github Action 2023-09-08 21:31:37 +00:00
Leo Giovanetti 350d84a047 fix: showing profile settings of other teams (#11245)
* fix: showing profile settings of other teams

* Updating URL
2023-09-08 18:29:06 -03:00
Shubham SinghandPeer Richelsen 9c3cbee363 feat: preference to show/hide available seats count in events (#11109)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-08 11:37:26 -04:00
Peer RichelsenandUdit Takkar 7e2ad3cea9 fix: inverted location logo (#11233)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-08 15:59:05 +01:00
Alex van Andel 4ef52aa2c6 perf: Move query to get just the user email to the main get query (#11230) 2023-09-08 08:39:29 -03:00
Alex van Andel 3132c64205 perf: Improve getAggregatedAvailability by reducing reliance on Day.js (#11224) 2023-09-08 07:19:27 -03:00
Crowdin Bot f25f8e0909 New Crowdin translations by Github Action 2023-09-08 08:05:45 +00:00
Leo Giovanetti d8961f8eb9 fix: subteam invitation without teamId associated (#11222) 2023-09-08 09:02:12 +01:00
Crowdin Bot 9ff7cf650b New Crowdin translations by Github Action 2023-09-08 00:23:57 +00:00
ba4e717b59 chore: unify org data access (#11158)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-09-07 21:21:04 -03:00
Udit Takkar f326a9a620 fix: eventName variables (#10924) 2023-09-07 16:52:07 -04:00
Omar López 95be16c358 v3.2.9
release.yml / release (push) Canceled after 0s
2023-09-07 13:14:59 -07:00
Peer Richelsen 5621507f7e chore: fixed an i18n string (#11200) 2023-09-07 17:27:22 +00:00
Mehul bc6d58fd47 fix: bad setState call at datePicker component (#11175) 2023-09-07 17:11:59 +00:00
WesleyandCarinaWolli db29cc1251 feat: send new booking uid on reschedule webhook (#10654)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-07 16:27:46 +00:00
Patel Divyesh 69888c57b0 fix: Fixed SVG icons on light and dark mode (#11173) 2023-09-07 17:34:11 +02:00
5c0da23b97 feat: User Avatar and Org Logo (#10700)
* Pass organization name & logo

* Overflow hidden

* Show org icon on public page

* Add org logo to large user avatars

* Clean up

* Add org name and logo to context

* Get org logo from /avatar.png endpoint

* Do not query for logo

* Remove name and logo from session middleware

* Type fix

* Set user onboarding org logo

* feat: organization avatar component (#10788)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Type fixes

* Type fix

* Transition to org slug for organization avatar

* Address feedback

* Clean up

* Clean up

* Type fix

* fix: set avatar cache control (#11163)

* test: Integration tests for handleNewBooking (#11044)

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

* fix: booking_paid webhook and added new payment metadata (#11093)

* app store improvements, logos, dark mode, added screenshots, fixed author names (#11164)

* fix: mobile event types and avatars (#11184)

* New Crowdin translations by Github Action

* fix: updateProfile metadata overwrite (#11188)

Co-authored-by: alannnc <alannnc@gmail.com>

* New Crowdin translations by Github Action

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-09-07 17:26:40 +02:00
Hariom BalharaandCarina Wollendorfer 5324ec8051 fix: Routing Form - Show responses in the order of fields (#11143)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-09-07 15:22:39 +00:00
Crowdin Bot b946f89ffb New Crowdin translations by Github Action 2023-09-07 12:33:00 +00:00
Crowdin Bot 3e3581fabc New Crowdin translations by Github Action 2023-09-07 12:29:44 +00:00
Crowdin Bot 05ac339233 New Crowdin translations by Github Action 2023-09-07 12:26:19 +00:00
Crowdin Bot ef4675ff44 New Crowdin translations by Github Action 2023-09-07 12:22:42 +00:00
sean-brydon 47dc85ee1b chore: fixing darkmode data table (#11195) 2023-09-07 14:18:52 +02:00
Crowdin Bot 50342e33ed New Crowdin translations by Github Action 2023-09-06 23:43:28 +00:00
Omar Lópezandalannnc eebcb636f1 fix: updateProfile metadata overwrite (#11188)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-09-06 16:40:50 -07:00
Crowdin Bot ed2ced899a New Crowdin translations by Github Action 2023-09-06 21:43:37 +00:00
Leo Giovanetti b7ae8c9594 fix: mobile event types and avatars (#11184) 2023-09-06 23:41:03 +02:00
Peer Richelsen fce98ebb13 app store improvements, logos, dark mode, added screenshots, fixed author names (#11164) 2023-09-07 01:17:04 +05:30
alannnc b81b221bf5 fix: booking_paid webhook and added new payment metadata (#11093) 2023-09-06 12:40:27 -07:00
f9eb335d0b test: Integration tests for handleNewBooking (#11044)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-06 12:23:53 -07:00
Omar López d7d7bcd651 fix: set avatar cache control (#11163) 2023-09-06 12:22:13 -07:00
Crowdin Bot 4c861b9e67 New Crowdin translations by Github Action 2023-09-06 18:32:09 +00:00
Carina WollendorferandCarinaWolli 2fa704c39b fix: add make invite link to setup page (#11177)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-06 14:29:13 -04:00
Crowdin Bot 9ccdf21d75 New Crowdin translations by Github Action 2023-09-06 15:25:19 +00:00
GitStart-Cal.comandgitstart-calcom 9d6148da42 chore: add Wizard in storybook (CALCOM-10760) (#11166)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 17:22:02 +02:00
MehulandUdit Takkar bf437b7d6b fix: event duration can be 0 (#11145)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-06 17:21:24 +02:00
Udit Takkar 3701fcd68b fix: teams page error (#11171) 2023-09-06 11:38:21 +02:00
GitStart-Cal.comandgitstart-calcom 2f90dc7cb6 fix: DataTable storybook file is not rendering (#10984)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 09:18:06 +01:00
alannnc 829f90b82f chore: remove unused queries on user event types (#11159)
* remove unused queries on user event types

* Fix types
2023-09-06 05:00:06 -03:00
Crowdin Bot 3539693729 New Crowdin translations by Github Action 2023-09-06 05:11:51 +00:00
Joe Au-Yeung 868ba4396d fix: Installing Zoho CRM (#11161)
* Fix installing Zoho CRM

* Clean up
2023-09-06 05:08:59 +00:00
zomars d06e36a5cd Update yarn.lock 2023-09-05 14:46:14 -07:00
Crowdin Bot f61c9d9bbf New Crowdin translations by Github Action 2023-09-05 21:39:15 +00:00
Leo Giovanetti a2d521e001 fix: i18n issues reported by crowdin (#11160) 2023-09-05 18:36:17 -03:00
Omar López bf9c881d16 v3.2.8
release.yml / release (push) Canceled after 0s
2023-09-05 13:41:18 -07:00
alannnc 6dd0f9ce29 fix: selected organizer when creating event (#11157) 2023-09-05 13:38:59 -07:00
d34db7aef6 fix: Fix wrong footer Button in some languages (#10873)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-09-05 18:44:46 +00:00
Keith WilliamsandAlan b4b90f2d27 perf: Removed unused queries for user event types (#10568)
* perf: Removed unused queries for user event types

* Added empty children array

* Refactor to clean up

* Fixing type issue

* Removed extra call to get raw event types

* Temporarily putting children back

* Removing include from children

* Added check for children

* Put children back for testing

---------

Co-authored-by: Alan <alannnc@gmail.com>
2023-09-05 11:35:02 -07:00
Peer Richelsen 3f0380617a chore: removed gravatar (#11153) 2023-09-05 19:56:02 +02:00
Udit Takkar 7143923ea4 fix: rescheduling in require confirmation (#11152)
## What does this PR do?

Fixes https://github.com/calcom/cal.com/issues/11075

BEFORE:

Meeting has been rescheduled email and calendar event after submitting booking without confirming the meeting (require confirmation event type).

<img width="1086" alt="Screenshot 2023-09-05 at 7 45 44 PM" src="https://github.com/calcom/cal.com/assets/53316345/574c8d2f-3698-44ea-9466-aef162e09dd5">

AFTER:-

event scheduled email and calendar event created only after "awaiting approval" meeting has been confirmed.

<img width="1086" alt="Screenshot 2023-09-05 at 7 47 22 PM" src="https://github.com/calcom/cal.com/assets/53316345/af19ccea-d418-4a40-a9c6-88782522df53">


## Type of change

<!-- Please delete bullets that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

1) Create a requires confirmation event type
2) open incognito tab or another browser and book a meeting (Attendee)
3) Confirm the booking (Organizer) and attendee would get meeting schedule email
4) Get the reschedule link from attendee's email and reschedule the booking

Both attendee and organiser shouldn't get booking email or calendar event  before organiser has accepted the meeting
2023-09-05 10:52:47 -07:00
DexterStorey dfccf4604f fix: better timezones and error handling in ai tools (#11085)
* current time in user timezone, let ai handle 401

* update agent.ts

* simple onboarding for non cal.com users

* reverting this line - unecessary

* v1.0.1
2023-09-05 13:56:26 +01:00
VijayandHariom Balhara c12abd75a8 fix: #10973 back button not working as expected in embed (#11073)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-05 15:40:09 +05:30
Crowdin Bot 2e12f09f05 New Crowdin translations by Github Action 2023-09-04 23:12:36 +00:00
alannnc 4258da1df5 remove double call on useSchedule() (#11141) 2023-09-04 23:10:19 +00:00
Hariom Balhara 41ef354c7b fix: FORM_SUBMITTED webhook payload change and support for Team Webhooks with it (#10986) 2023-09-04 17:04:57 -04:00
alannncandzomars 8891641953 hotfix: missing gCal calendarId (#11139)
Co-authored-by: zomars <zomars@me.com>
2023-09-04 20:25:39 +00:00
Udit Takkar 0150041496 fix: improve UX of team invite by copy link (#11009) 2023-09-04 12:43:37 -07:00
Carina WollendorferandCarinaWolli 13f9e97015 fix: variables for custom webhook payload (#11135)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 11:01:12 -07:00
Peer Richelsen 3c5901edb8 chore: cal.ai redirect (#11130) 2023-09-04 16:21:37 +00:00
Madhan KumarandAlan d26bab5880 fix: Require an 'Email' placeholder in the 'Add guest' field. (#11123)
* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* fixes

* fixes

---------

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-09-04 09:13:12 -07:00
Udit Takkar 1c451a8d51 fix: installed badge (#11131) 2023-09-04 17:51:59 +02:00
Crowdin Bot 576261bf88 New Crowdin translations by Github Action 2023-09-04 15:50:49 +00:00
alannnc 7d944af7f6 fix: teams are listed on insights but then you cant find them on settings page 11087 cal 2401 (#11092)
* fix listing of other teams

* add more invalidate trpc queries

* Remove unused code

* Removed unnecesary flags
2023-09-04 08:47:53 -07:00
Crowdin Bot dfe7056c93 New Crowdin translations by Github Action 2023-09-04 14:55:48 +00:00
b27f01449a feat: make.com/integromat app (#8897)
* start make app integration

* setup integration

* add relevant env vars

* update app metadata

* import setup route in app setups

* fix typo

* add app store imports

* fix module import error

* update make readme

* move scheduler to app-store utils

* move add subscription to node scheduler

* move delete subscription to scheduler

* subscribe unsubscribe in zapier using common nodeScheduler

* fix lint errors

* revert settings.json

* update icon

* add app screenshots

* fix app description

* fix type errors

* update app code

* Delete .gitkeep

* delete unused template files

* get app invite link from env vars

* chore: handle error, cleanup readme, address review comments

* fix: update link in readme

* revert yarn.lock

* fix type errors

* Update packages/prisma/seed-app-store.ts

* Update .env.appStore.example

* Update .env.appStore.example

* update app readme

* fix param name in deleteSubcription

* fix listBookings handler

* Update turbo.json

* use default installation handler to install app

* use logger for console logs

* Fix inviteLink reading

* fix app setup handler

* Fix type issue

* always show app invite link

* fix type error

* add make invite link

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 14:52:34 +00:00
Udit Takkar 6dd77d0494 fix: create event type (#11046) 2023-09-04 15:02:30 +02:00
f73d38b632 fix: color tokens are not used in payment form (#10806)
Co-authored-by: VIneet Pradhan <138296762+vineetpradhan-dev@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-04 15:02:21 +02:00
Gaurav SutharandUdit Takkar a7d8f0c84e fix: add loading to delete event dialog (#11102)
* add loading to delete event dialog

* chore: use mutation isLoading

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-09-04 14:59:50 +02:00
Heath Henley b0164990c0 Conformation --> Confirmation (#11058) 2023-09-04 14:56:46 +02:00
Ikko Eltociear Ashimine bad72ba7bf chore: fix typo in embed-iframe.ts (#10858)
currenty -> currently
2023-09-04 17:48:42 +05:30
Crowdin Bot 1b317a98a4 New Crowdin translations by Github Action 2023-09-04 12:17:54 +00:00
Omar López 6a22d75caa fix: no-scrollbar for firefox (#10964) 2023-09-04 17:43:32 +05:30
a030861423 feat: recently added apps (#11118)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 10:30:21 +00:00
omahs bd9cd00c04 fix: typos (#11125) 2023-09-04 12:18:02 +02:00
Kushagra MathurandPeer Richelsen 6fd34af6f6 fix: dark mode icons for Riverside and Sirius Video (#11107)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 09:55:16 +00:00
1a85f336c5 fix: Booking Question Dialog: Dropdown covered by DialogFooter (CALCOM-10987) (#10989)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 11:42:10 +02:00
Peer Richelsen cc5096a54a chore: stop using telemetry for website (#11050) 2023-09-01 16:53:08 -07:00
alannnc eea7059226 Add test for different layouts (#11091) 2023-09-01 16:26:45 -07:00
Omar López 63f3247b1b fix: Vercel preview rewrites 404s (#11090) 2023-09-01 23:36:59 +02:00
Hariom BalharaandJoe Au-Yeung 7a769c38fb Remove duplicate fields (#11003)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-02 00:58:21 +05:30
alannnc 34c2565c70 fix paypal minor issues (#11069) 2023-09-01 11:03:27 -07:00
Crowdin Bot a130804f18 New Crowdin translations by Github Action 2023-09-01 17:55:12 +00:00
Leo Giovanetti 446e851065 fix: support subteam in org redirection (#11080) 2023-09-01 19:52:27 +02:00
Abhilash Sampath f06b4bfb9c fix: Add send sms description (#11063) 2023-09-01 19:12:10 +02:00
Alex van Andel 1896efa3a0 v3.2.7.1
release.yml / release (push) Canceled after 0s
2023-09-01 17:45:59 +01:00
Alex van Andel 62c8603863 fix: Revert #10366: Undoing then redo fix differently (#11078)
* Revert #10366: Undoing then redo fix differently

* Re-implement defaulting to first day in the month

* Update Embed.tsx and DateOverrideInputDialog
2023-09-01 17:12:01 +01:00
7d8559b0b0 chore: add Dropdown in storybook (#10874)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-01 18:08:34 +02:00
sean-brydon 7a48bbe553 Remove background colour (#11082) 2023-09-01 15:55:38 +00:00
nidhi sharma 817e95c246 fix #11077 space added on team events (#11081) 2023-09-01 15:30:37 +00:00
Udit Takkar 4ac6225a35 fix: unauthorized error in organization (#11039)
* fix: unauthorized error in organization

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

* chore

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

* fix: allow instance admin to update

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-01 14:02:58 +00:00
sean-brydon 38d9b93dee Fix avatar (#11076) 2023-09-01 14:23:28 +01:00
sean-brydonandUdit Takkar 376b38d4ed fix: Fixes username invite issue (#10998)
* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-01 17:31:45 +05:30
Hariom Balhara 0e181f6d9f Revert "feat: adds next cold start profiler (#11014)" (#11072)
This reverts commit 05631d093f.
2023-09-01 10:18:17 +01:00
Omar López 4dbb692156 v3.2.7
release.yml / release (push) Canceled after 0s
2023-08-31 15:50:53 -07:00
alannnc 0068718d24 fix sub quantity update stripe (#11057) 2023-08-31 22:46:48 +00:00
alannnc ac817b3542 easy fix (#11054) 2023-08-31 22:44:35 +00:00
zomars 97f2cffd1f fix: zod utils due to upgrade 2023-08-31 15:27:51 -07:00
zomars b6dea9c161 Sync packages 2023-08-31 12:57:59 -07:00
57eb4746ad chore: add Switch in storybook (CALCOM-10760) (#10804)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 21:36:30 +02:00
eff9c87964 chore: add Timezone Select in storybook (CALCOM-10760) (#10966)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 21:34:22 +02:00
e06c07bfec fix: email embed – remove collapsible and permanently show times (#10996)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Mehul <mehulzr@gmail.com>
2023-08-31 19:32:52 +00:00
GitStart-Cal.comandgitstart-calcom 332c00dac2 fix: Fix tooltip control on ButtonPlayground storybook file (fix-ButtonTooltip) (#10937)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:17:59 +02:00
GitStart-Cal.comandgitstart-calcom e3523db6bb fix: Add controls for Select Field storybook file (#10936)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:17:09 +02:00
GitStart-Cal.comandgitstart-calcom 9c0f046b2a fix: Logo storybook file with invalid icons (fix-logo) (#11018)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:16:21 +02:00
alannnc 6e1a717ecf fix: add metadata to stripe payment intent (#11053) 2023-08-31 19:15:45 +00:00
b2e098c781 chore: add ToggleGroup in storybook (#10802)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-08-31 21:12:13 +02:00
Leo GiovanettiandPeer Richelsen 3010900358 fix: admin org list without members (#11051)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 19:11:28 +00:00
GitStart-Cal.comandgitstart-calcom 9b043f44a0 fix: List storybook file is empty (fix-list) (#10965)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 21:11:01 +02:00
DexterStorey 814674fe40 resolve zod versions across child packages (#11052) 2023-08-31 20:10:59 +01:00
acaa79b0b8 feat: sorting for workflow and routing forms (#10780)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 18:57:33 +00:00
GitStart-Cal.comandgitstart-calcom 781a0f4a7c chore: add ErrorBoundary in storybook (CALCOM-10760) (#10872)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 20:56:50 +02:00
Patel Divyesh b2028870d0 fix: broken company profile link on team booking page (#10978) 2023-08-31 20:45:26 +02:00
8f70be8861 fix: Error when running storybook (#11037)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-31 20:35:43 +02:00
alannnc 53c8f8c04a fix: include app data and credentials from DB (#11048)
* include app data and credentials from DB

* Improve performance
2023-08-31 11:10:58 -07:00
alannnc c4e5d168a5 fix: handle collective multiple host on destinationCalendar (#10967) 2023-08-31 10:47:02 -07:00
sean-brydon 500bfb4d82 chore: sheet darkmode and improve responsive (#11047) 2023-08-31 19:09:06 +02:00
Sahil Padvi 024b92b9ce styles:dark mode color fix (#11004) 2023-08-31 17:18:27 +01:00
Crowdin Bot f23b63cc98 New Crowdin translations by Github Action 2023-08-31 14:24:31 +00:00
Hariom Balhara edc90e3f27 Handle disabling of ORGANIZATIONS_ENABLED flag (#11041) 2023-08-31 19:48:20 +05:30
Omar López 05631d093f feat: adds next cold start profiler (#11014) 2023-08-31 12:25:52 +02:00
GitStart-Cal.comandgitstart-calcom 6000ff4b48 chore: add ColorPicker in storybook (CALCOM-10760) (#10866)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-31 12:16:37 +02:00
Anik Dhabal Babu cd7de98e3f fix: Set mobile availability (#11027) 2023-08-31 12:08:34 +02:00
3f51858d27 chore: add Popover in storybook (#11021)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-31 12:04:09 +02:00
Denzil SamuelandUdit Takkar 2c498ace5c fix: fix-tablet-menu-not-centered-sidebar (#11020)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-31 12:03:32 +02:00
Kamil B. Demirci 595d781d51 refactor: improvements on german translation (#10898) 2023-08-31 12:03:11 +02:00
Udit Takkar 3c508568da fix: lower case slugs in teams (#11026)
* fix: lower case slugs in teams

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

* fix: use slugify

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

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-30 23:34:19 -07:00
Abhijeet Singhandalannnc 3204d0af34 feat: team admin: see connected apps of team members (#11036)
* added feature:team admin can see connected apps of members

* fixed the type error

* Update packages/lib/server/queries/teams/index.ts

* Minor fixes

---------

Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-31 01:20:31 +00:00
25684f9040 fix: meeting ended trigger for webhooks and zapier sometimes not working (#10946)
Co-authored-by: mohammed gehad <mohammed.gehad.1998@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-30 23:17:42 +00:00
Crowdin Bot bc9aeef710 New Crowdin translations by Github Action 2023-08-30 23:13:45 +00:00
393411a47a feat: cal ai (#10992)
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: tedspare <ted.spare@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-30 16:10:59 -07:00
Leo Giovanetti 356117feaf fix: other reported issues (#11015)
* fix: weird margin top in avatar

* fix: pending users are shown on booking page

* fix: avatar and naming issues

* fix: toast alignment and removing unneeded titles

* missing changes from toast improvements

* feat: empty state for teams without event types

* Removing console.log
2023-08-30 13:42:34 -07:00
Leo Giovanetti 1bf68d50e0 fix: multiple duration when booking (#11032) 2023-08-30 12:19:00 -07:00
Crowdin Bot 4a35e85756 New Crowdin translations by Github Action 2023-08-30 10:15:41 +00:00
Peer Richelsenandalannnc a4f56bf680 Delete add-PRs-to-project-reviewing-PRs.yml (#11008)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-08-30 10:12:54 +00:00
nicktrnandPeer Richelsen a308075bc3 feat: 2fa backup codes (#10600)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-30 07:33:48 +00:00
efa6d464a3 refactor: removed redundant test (#10785)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-29 18:14:30 +00:00
Crowdin Bot 811262d7ee New Crowdin translations by Github Action 2023-08-29 17:29:54 +00:00
Udit Takkar 99a5dcf96a fix: type error in booker (#11011) 2023-08-29 10:27:06 -07:00
MehulandUdit Takkar 79f092673a fix: signup password hint validation (#10454)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-29 13:56:45 +02:00
b285f27d00 feat: option for adding ics events to workflow reminders (#10856)
Co-authored-by: SMLukwiya <sundaymorganl@gmail.com>
Co-authored-by: Monto <138862352+monto7926@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-29 13:56:26 +02:00
1fa87ae179 fix: Modal popup overflow issue in reschedule booking (#10976)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-29 12:36:15 +02:00
Patel DivyeshandPeer Richelsen f6f4b67bf9 fix: Team Logo not Showing up in Team Event Create Modal (#10959)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-29 12:35:50 +02:00
MehulandHariom Balhara 8c8550e6f8 fix: switching between months leads to incorrect autoselect first available date (#10366)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-29 12:32:56 +02:00
Joe Au-YeungandHariom Balhara c6246c9408 refactor: Eventtype app context after orgs (#10781)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-29 11:32:20 +05:30
495 changed files with 14448 additions and 6317 deletions
+4
View File
@@ -213,6 +213,10 @@ NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
# use organizations
ORGANIZATIONS_ENABLED=
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
# existing organizations based on email domain address
ORGANIZATIONS_AUTOLINK=
# Vercel Config to create subdomains for organizations
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
PROJECT_ID_VERCEL=
@@ -1,16 +0,0 @@
name: Add PRs to project Reviewing PRs
on:
pull_request:
types:
- opened
jobs:
add-PR-to-project:
name: Add PRs to project Reviewing PRs
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/11
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
@@ -0,0 +1,23 @@
name: Cron - webhookTriggers
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “every 5 minutes” (see https://crontab.guru)
- cron: "*/5 * * * *"
jobs:
cron-webhookTriggers:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/webhookTriggers \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-1
View File
@@ -7,7 +7,6 @@ public
*.lock
*.log
*.test.ts
.gitignore
.npmignore
+4 -5
View File
@@ -1,6 +1,6 @@
# Contributing to Cal.com
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
@@ -37,7 +37,7 @@ Contributions are what make the open source community such an amazing place to l
</tr>
<tr>
<td>
Core Features (Booking page, availabilty, timezone calculation)
Core Features (Booking page, availability, timezone calculation)
</td>
<td>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
@@ -132,7 +132,6 @@ If you get errors, be sure to fix them before committing.
## Making a Pull Request
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to fill the PR Template accordingly.
+4 -4
View File
@@ -423,7 +423,7 @@ yarn seed-app-store
```
You will need to complete a few more steps to activate Google Calendar App.
Make sure to complete section "Obtaining the Google API Credentials". After the do the
Make sure to complete section "Obtaining the Google API Credentials". After that do the
following
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
@@ -449,8 +449,8 @@ following
7. Click "Create".
8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
10. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
@@ -526,7 +526,7 @@ For example, `Cal.com (support@cal.com)`.
3. Copy Account SID to your `.env` file into the `TWILIO_SID` field
4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field
5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field
6. Add your own sender id to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
6. Add your own sender ID to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
7. Create a messaging service (Develop -> Messaging -> Services)
8. Choose any name for the messaging service
9. Click 'Add Senders'
+15
View File
@@ -0,0 +1,15 @@
BACKEND_URL=http://localhost:3002/api
APP_ID=cal-ai
APP_URL=http://localhost:3000/apps/cal-ai
# Used to verify requests from sendgrid. You can generate a new one with: `openssl rand -hex 32`
PARSE_KEY=
OPENAI_API_KEY=
# Optionally trace completions at https://smith.langchain.com
# LANGCHAIN_TRACING_V2=true
# LANGCHAIN_ENDPOINT=
# LANGCHAIN_API_KEY=
# LANGCHAIN_PROJECT=
+48
View File
@@ -0,0 +1,48 @@
# Cal.com Email Assistant
Welcome to the first stage of Cal AI!
This app lets you chat with your calendar via email:
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
- List and rearrange your bookings eg. "Cancel my next meeting"
- Answer basic questions about your busiest times eg. "How does my Tuesday look?"
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
_The AI agent can only choose from a set of tools, without ever seeing your API key._
Emails are cleaned and routed in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts) using [MailParser](https://nodemailer.com/extras/mailparser/).
Incoming emails are routed by email address. Addresses are verified by [DKIM record](https://support.google.com/a/answer/174124?hl=en), making it hard to spoof them.
## Getting Started
### Development
If you haven't yet, please run the [root setup](/README.md) steps.
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
- A default sender email (for example, `ai@cal.dev`)
- The Cal AI's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
### Email Router
To expose the AI app, run `ngrok http 3000` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
To forward incoming emails to the Node.js server, one option is to use [SendGrid's Inbound Parse Webhook](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
1. [Sign up for an account](https://signup.sendgrid.com/)
2. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL.
3. For subdomain, use `<sub>.<domain>.com` for now, where `sub` can be any subdomain but `domain.com` will need to be verified via MX records in your environment variables, eg. on [Vercel](https://vercel.com/guides/how-to-add-vercel-environment-variables).
4. Use the nGrok URL from above as the **Destination URL**.
5. Activate "POST the raw, full MIME message".
6. Send an email to `<anyone>@ai.example.com`. You should see a ping on the nGrok listener and Node.js server.
7. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
Please feel free to improve any part of this architecture.
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+24
View File
@@ -0,0 +1,24 @@
const withBundleAnalyzer = require("@next/bundle-analyzer");
const plugins = [];
plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "https://cal.com/ai",
permanent: true,
},
];
},
i18n: {
defaultLocale: "en",
locales: ["en"],
},
reactStrictMode: true,
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@calcom/ai",
"version": "1.0.1",
"private": true,
"author": "Cal.com Inc.",
"dependencies": {
"@calcom/prisma": "*",
"@t3-oss/env-nextjs": "^0.6.1",
"langchain": "^0.0.131",
"mailparser": "^3.6.5",
"next": "^13.4.6",
"supports-color": "8.1.1",
"zod": "^3.22.2"
},
"devDependencies": {
"@types/mailparser": "^3.4.0"
},
"scripts": {
"build": "next build",
"dev": "next dev -p 3005",
"format": "npx prettier . --write",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "next start"
}
}
+45
View File
@@ -0,0 +1,45 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import agent from "../../../utils/agent";
import sendEmail from "../../../utils/sendEmail";
import { verifyParseKey } from "../../../utils/verifyParseKey";
/**
* Launches a LangChain agent to process an incoming email,
* then sends the response to the user.
*/
export const POST = async (request: NextRequest) => {
const verified = verifyParseKey(request.url);
if (!verified) {
return new NextResponse("Unauthorized", { status: 401 });
}
const json = await request.json();
const { apiKey, userId, message, subject, user, replyTo } = json;
if ((!message && !subject) || !user) {
return new NextResponse("Missing fields", { status: 400 });
}
try {
const response = await agent(`${subject}\n\n${message}`, user, apiKey, userId);
// Send response to user
await sendEmail({
subject: `Re: ${subject}`,
text: response.replace(/(?:\r\n|\r|\n)/g, "\n"),
to: user.email,
from: replyTo,
});
return new NextResponse("ok");
} catch (error) {
return new NextResponse(
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
{ status: 500 }
);
}
};
+154
View File
@@ -0,0 +1,154 @@
import type { ParsedMail, Source } from "mailparser";
import { simpleParser } from "mailparser";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import prisma from "@calcom/prisma";
import { env } from "../../../env.mjs";
import { fetchAvailability } from "../../../tools/getAvailability";
import { fetchEventTypes } from "../../../tools/getEventTypes";
import getHostFromHeaders from "../../../utils/host";
import now from "../../../utils/now";
import sendEmail from "../../../utils/sendEmail";
import { verifyParseKey } from "../../../utils/verifyParseKey";
/**
* Verifies email signature and app authorization,
* then hands off to booking agent.
*/
export const POST = async (request: NextRequest) => {
const verified = verifyParseKey(request.url);
if (!verified) {
return new NextResponse("Unauthorized", { status: 401 });
}
const formData = await request.formData();
const body = Object.fromEntries(formData);
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
const signature = (body.dkim as string).includes(" : pass");
const envelope = JSON.parse(body.envelope as string);
const aiEmail = envelope.to[0];
// Parse email from mixed MIME type
const parsed: ParsedMail = await simpleParser(body.email as Source);
if (!parsed.text && !parsed.subject) {
return new NextResponse("Email missing text and subject", { status: 400 });
}
const user = await prisma.user.findUnique({
select: {
email: true,
id: true,
timeZone: true,
credentials: {
select: {
appId: true,
key: true,
},
},
},
where: { email: envelope.from },
});
// User is not a cal.com user or is using an unverified email.
if (!signature || !user) {
await sendEmail({
html: `Thanks for your interest in Cal AI! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
subject: `Re: ${body.subject}`,
text: `Thanks for your interest in Cal AI! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse("ok");
}
const credential = user.credentials.find((c) => c.appId === env.APP_ID)?.key;
// User has not installed the app from the app store. Direct them to install it.
if (!(credential as { apiKey: string })?.apiKey) {
const url = env.APP_URL;
await sendEmail({
html: `Thanks for using Cal AI! To get started, the app must be installed. <a href=${url} target="_blank">Click this link</a> to install it.`,
subject: `Re: ${body.subject}`,
text: `Thanks for using Cal AI! To get started, the app must be installed. Click this link to install the Cal AI app: ${url}`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse("ok");
}
const { apiKey } = credential as { apiKey: string };
// Pre-fetch data relevant to most bookings.
const [eventTypes, availability] = await Promise.all([
fetchEventTypes({
apiKey,
}),
fetchAvailability({
apiKey,
userId: user.id,
dateFrom: now(user.timeZone),
dateTo: now(user.timeZone),
}),
]);
if ("error" in availability) {
await sendEmail({
subject: `Re: ${body.subject}`,
text: "Sorry, there was an error fetching your availability. Please try again.",
to: user.email,
from: aiEmail,
});
console.error(availability.error);
return new NextResponse("Error fetching availability. Please try again.", { status: 400 });
}
if ("error" in eventTypes) {
await sendEmail({
subject: `Re: ${body.subject}`,
text: "Sorry, there was an error fetching your event types. Please try again.",
to: user.email,
from: aiEmail,
});
console.error(eventTypes.error);
return new NextResponse("Error fetching event types. Please try again.", { status: 400 });
}
const { workingHours } = availability;
const appHost = getHostFromHeaders(request.headers);
// Hand off to long-running agent endpoint to handle the email. (don't await)
fetch(`${appHost}/api/agent?parseKey=${env.PARSE_KEY}`, {
body: JSON.stringify({
apiKey,
userId: user.id,
message: parsed.text,
subject: parsed.subject,
replyTo: aiEmail,
user: {
email: user.email,
eventTypes,
timeZone: user.timeZone,
workingHours,
},
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
await new Promise((r) => setTimeout(r, 1000));
return new NextResponse("ok");
};
+43
View File
@@ -0,0 +1,43 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
BACKEND_URL: process.env.BACKEND_URL,
APP_ID: process.env.APP_ID,
APP_URL: process.env.APP_URL,
PARSE_KEY: process.env.PARSE_KEY,
NODE_ENV: process.env.NODE_ENV,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
DATABASE_URL: process.env.DATABASE_URL,
},
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
BACKEND_URL: z.string().url(),
APP_ID: z.string().min(1),
APP_URL: z.string().url(),
PARSE_KEY: z.string().min(1),
NODE_ENV: z.enum(["development", "test", "production"]),
OPENAI_API_KEY: z.string().min(1),
SENDGRID_API_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
},
});
@@ -0,0 +1,110 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Creates a booking for a user by event type, times, and timezone.
*/
const createBooking = async ({
apiKey,
userId,
eventTypeId,
start,
end,
timeZone,
language,
responses,
}: {
apiKey: string;
userId: number;
eventTypeId: number;
start: string;
end: string;
timeZone: string;
language: string;
responses: { name?: string; email?: string; location?: string };
title?: string;
status?: string;
}): Promise<string | Error | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({
end,
eventTypeId,
language,
metadata: {},
responses,
start,
timeZone,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
// Let GPT handle this. This will happen when wrong event type id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return {
error: data.message,
};
}
return "Booking created";
};
const createBookingTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description:
"Tries to create a booking. If the user is unavailable, it will return availability that day, allowing you to avoid the getAvailability step in many cases.",
func: async ({ eventTypeId, start, end, timeZone, language, responses, title, status }) => {
return JSON.stringify(
await createBooking({
apiKey,
userId,
end,
eventTypeId,
language,
responses,
start,
status,
timeZone,
title,
})
);
},
name: "createBookingIfAvailable",
schema: z.object({
end: z
.string()
.describe("This should correspond to the event type's length, unless otherwise specified."),
eventTypeId: z.number(),
language: z.string(),
responses: z
.object({
email: z.string().optional(),
name: z.string().optional(),
})
.describe("External invited user. Not the user making the request."),
start: z.string(),
status: z.string().optional().describe("ACCEPTED, PENDING, CANCELLED or REJECTED"),
timeZone: z.string(),
title: z.string().optional(),
}),
});
};
export default createBookingTool;
+66
View File
@@ -0,0 +1,66 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Cancels a booking for a user by ID with reason.
*/
const cancelBooking = async ({
apiKey,
id,
reason,
}: {
apiKey: string;
id: string;
reason: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}/cancel?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ reason }),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking cancelled";
};
const cancelBookingTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Cancel a booking",
func: async ({ id, reason }) => {
return JSON.stringify(
await cancelBooking({
apiKey,
id,
reason,
})
);
},
name: "cancelBooking",
schema: z.object({
id: z.string(),
reason: z.string(),
}),
});
};
export default cancelBookingTool;
+82
View File
@@ -0,0 +1,82 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Availability } from "../types/availability";
/**
* Fetches availability for a user by date range and event type.
*/
export const fetchAvailability = async ({
apiKey,
userId,
dateFrom,
dateTo,
eventTypeId,
}: {
apiKey: string;
userId: number;
dateFrom: string;
dateTo: string;
eventTypeId?: number;
}): Promise<Partial<Availability> | { error: string }> => {
const params: { [k: string]: string } = {
apiKey,
userId: userId.toString(),
dateFrom,
dateTo,
};
if (eventTypeId) params["eventTypeId"] = eventTypeId.toString();
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/availability?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return {
busy: data.busy,
dateRanges: data.dateRanges,
timeZone: data.timeZone,
workingHours: data.workingHours,
};
};
const getAvailabilityTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Get availability within range.",
func: async ({ dateFrom, dateTo, eventTypeId }) => {
return JSON.stringify(
await fetchAvailability({
apiKey,
userId,
dateFrom,
dateTo,
eventTypeId,
})
);
},
name: "getAvailability",
schema: z.object({
dateFrom: z.string(),
dateTo: z.string(),
eventTypeId: z
.number()
.optional()
.describe(
"The ID of the event type to filter availability for if you've called getEventTypes, otherwise do not include."
),
}),
});
};
export default getAvailabilityTool;
+75
View File
@@ -0,0 +1,75 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { Booking } from "../types/booking";
import { BOOKING_STATUS } from "../types/booking";
/**
* Fetches bookings for a user by date range.
*/
const fetchBookings = async ({
apiKey,
userId,
from,
to,
}: {
apiKey: string;
userId: number;
from: string;
to: string;
}): Promise<Booking[] | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
const bookings: Booking[] = data.bookings
.filter((booking: Booking) => {
const afterFrom = new Date(booking.startTime).getTime() > new Date(from).getTime();
const beforeTo = new Date(booking.endTime).getTime() < new Date(to).getTime();
const notCancelled = booking.status !== BOOKING_STATUS.CANCELLED;
return afterFrom && beforeTo && notCancelled;
})
.map(({ endTime, eventTypeId, id, startTime, status, title }: Booking) => ({
endTime,
eventTypeId,
id,
startTime,
status,
title,
}));
return bookings;
};
const getBookingsTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Get bookings for a user between two dates.",
func: async ({ from, to }) => {
return JSON.stringify(await fetchBookings({ apiKey, userId, from, to }));
},
name: "getBookings",
schema: z.object({
from: z.string().describe("ISO 8601 datetime string"),
to: z.string().describe("ISO 8601 datetime string"),
}),
});
};
export default getBookingsTool;
+51
View File
@@ -0,0 +1,51 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
import type { EventType } from "../types/eventType";
/**
* Fetches event types by user ID.
*/
export const fetchEventTypes = async ({ apiKey }: { apiKey: string }) => {
const params = {
apiKey,
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/event-types?${urlParams.toString()}`;
const response = await fetch(url);
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return data.event_types.map((eventType: EventType) => ({
id: eventType.id,
length: eventType.length,
title: eventType.title,
}));
};
const getEventTypesTool = (apiKey: string) => {
return new DynamicStructuredTool({
description: "Get the user's event type IDs. Usually necessary to book a meeting.",
func: async () => {
return JSON.stringify(
await fetchEventTypes({
apiKey,
})
);
},
name: "getEventTypes",
schema: z.object({}),
});
};
export default getEventTypesTool;
+85
View File
@@ -0,0 +1,85 @@
import { DynamicStructuredTool } from "langchain/tools";
import { z } from "zod";
import { env } from "../env.mjs";
/**
* Edits a booking for a user by booking ID with new times, title, description, or status.
*/
const editBooking = async ({
apiKey,
userId,
id,
startTime, // In the docs it says start, but it's startTime: https://cal.com/docs/enterprise-features/api/api-reference/bookings#edit-an-existing-booking.
endTime, // Same here: it says end but it's endTime.
title,
description,
status,
}: {
apiKey: string;
userId: number;
id: string;
startTime?: string;
endTime?: string;
title?: string;
description?: string;
status?: string;
}): Promise<string | { error: string }> => {
const params = {
apiKey,
userId: userId.toString(),
};
const urlParams = new URLSearchParams(params);
const url = `${env.BACKEND_URL}/bookings/${id}?${urlParams.toString()}`;
const response = await fetch(url, {
body: JSON.stringify({ description, endTime, startTime, status, title }),
headers: {
"Content-Type": "application/json",
},
method: "PATCH",
});
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
if (response.status !== 200) {
return { error: data.message };
}
return "Booking edited";
};
const editBookingTool = (apiKey: string, userId: number) => {
return new DynamicStructuredTool({
description: "Edit a booking",
func: async ({ description, endTime, id, startTime, status, title }) => {
return JSON.stringify(
await editBooking({
apiKey,
userId,
description,
endTime,
id,
startTime,
status,
title,
})
);
},
name: "editBooking",
schema: z.object({
description: z.string().optional(),
endTime: z.string().optional(),
id: z.string(),
startTime: z.string().optional(),
status: z.string().optional(),
title: z.string().optional(),
}),
});
};
export default editBookingTool;
+25
View File
@@ -0,0 +1,25 @@
export type Availability = {
busy: {
start: string;
end: string;
title?: string;
}[];
timeZone: string;
dateRanges: {
start: string;
end: string;
}[];
workingHours: {
days: number[];
startTime: number;
endTime: number;
userId: number;
}[];
dateOverrides: {
date: string;
startTime: number;
endTime: number;
userId: number;
};
currentSeats: number;
};
+23
View File
@@ -0,0 +1,23 @@
export enum BOOKING_STATUS {
ACCEPTED = "ACCEPTED",
PENDING = "PENDING",
CANCELLED = "CANCELLED",
REJECTED = "REJECTED",
}
export type Booking = {
id: number;
userId: number;
description: string | null;
eventTypeId: number;
uid: string;
title: string;
startTime: string;
endTime: string;
attendees: { email: string; name: string; timeZone: string; locale: string }[] | null;
user: { email: string; name: string; timeZone: string; locale: string }[] | null;
payment: { id: number; success: boolean; paymentOption: string }[];
metadata: object | null;
status: BOOKING_STATUS;
responses: { email: string; name: string; location: string } | null;
};
+13
View File
@@ -0,0 +1,13 @@
export type EventType = {
id: number;
title: string;
length: number;
metadata: object;
slug: string;
hosts: {
userId: number;
isFixed: boolean;
}[];
hidden: boolean;
// ...
};
+9
View File
@@ -0,0 +1,9 @@
import type { EventType } from "./eventType";
import type { WorkingHours } from "./workingHours";
export type User = {
email: string;
timeZone: string;
eventTypes: EventType[];
workingHours: WorkingHours[];
};
+5
View File
@@ -0,0 +1,5 @@
export type WorkingHours = {
days: number[];
startTime: number;
endTime: number;
};
+73
View File
@@ -0,0 +1,73 @@
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { env } from "../env.mjs";
import createBookingIfAvailable from "../tools/createBookingIfAvailable";
import deleteBooking from "../tools/deleteBooking";
import getAvailability from "../tools/getAvailability";
import getBookings from "../tools/getBookings";
import updateBooking from "../tools/updateBooking";
import type { EventType } from "../types/eventType";
import type { User } from "../types/user";
import type { WorkingHours } from "../types/workingHours";
import now from "./now";
const gptModel = "gpt-4";
/**
* Core of the Cal AI booking agent: a LangChain Agent Executor.
* Uses a toolchain to book meetings, list available slots, etc.
* Uses OpenAI functions to better enforce JSON-parsable output from the LLM.
*/
const agent = async (input: string, user: User, apiKey: string, userId: number) => {
const tools = [
createBookingIfAvailable(apiKey, userId),
getAvailability(apiKey, userId),
getBookings(apiKey, userId),
updateBooking(apiKey, userId),
deleteBooking(apiKey),
];
const model = new ChatOpenAI({
modelName: gptModel,
openAIApiKey: env.OPENAI_API_KEY,
temperature: 0,
});
/**
* Initialize the agent executor with arguments.
*/
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentArgs: {
prefix: `You are Cal AI - a bleeding edge scheduling assistant that interfaces via email.
Make sure your final answers are definitive, complete and well formatted.
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to the user should be formatted per that user's timezone.
The current time in the user's timezone is: ${now(user.timeZone)}
The user's time zone is: ${user.timeZone}
The user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Title: ${e.title}, Length: ${e.length}`)
.join("\n")}
The user's working hours are: ${user.workingHours
.map(
(w: WorkingHours) =>
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
w.startTime
}, End Time (minutes in UTC): ${w.endTime}`
)
.join("\n")}
`,
},
agentType: "openai-functions",
returnIntermediateSteps: env.NODE_ENV === "development",
verbose: env.NODE_ENV === "development",
});
const result = await executor.call({ input });
const { output } = result;
return output;
};
export default agent;
+7
View File
@@ -0,0 +1,7 @@
import type { NextRequest } from "next/server";
const getHostFromHeaders = (headers: NextRequest["headers"]): string => {
return `https://${headers.get("host")}`;
};
export default getHostFromHeaders;
+5
View File
@@ -0,0 +1,5 @@
export default function now(timeZone: string) {
return new Date().toLocaleString("en-US", {
timeZone,
});
}
+40
View File
@@ -0,0 +1,40 @@
import mail from "@sendgrid/mail";
const sendgridAPIKey = process.env.SENDGRID_API_KEY as string;
/**
* Simply send an email by address, subject, and body.
*/
const send = async ({
subject,
to,
from,
text,
html,
}: {
subject: string;
to: string;
from: string;
text: string;
html?: string;
}): Promise<boolean> => {
mail.setApiKey(sendgridAPIKey);
const msg = {
to,
from: {
email: from,
name: "Cal AI",
},
text,
html,
subject,
};
const res = await mail.send(msg);
const success = !!res;
return success;
};
export default send;
+13
View File
@@ -0,0 +1,13 @@
import type { NextRequest } from "next/server";
import { env } from "../env.mjs";
/**
* Verifies that the request contains the correct parse key.
* env.PARSE_KEY must be configured as a query param in the sendgrid inbound parse settings.
*/
export const verifyParseKey = (url: NextRequest["url"]) => {
const verified = new URL(url).searchParams.get("parseKey") === env.PARSE_KEY;
return verified;
};
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"compilerOptions": {
"strict": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"~/*": ["*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+3
View File
@@ -70,6 +70,7 @@ const schemaEventTypeCreateParams = z
recurringEvent: recurringEventInputSchema.optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
@@ -89,6 +90,7 @@ const schemaEventTypeEditParams = z
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
@@ -129,6 +131,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
durationLimits: true,
+1 -1
View File
@@ -40,6 +40,6 @@
"typescript": "^4.9.4",
"tzdata": "^1.0.30",
"uuid": "^8.3.2",
"zod": "^3.20.2"
"zod": "^3.22.2"
}
}
+3
View File
@@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server";
* seatsShowAttendees:
* type: boolean
* description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* smsReminderNumber:
* type: number
* description: 'SMS reminder number'
@@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* seatsShowAttendees:
* type: boolean
* description: 'Share Attendee information in seats'
* seatsShowAvailabilityCount:
* type: boolean
* description: 'Show the number of available seats'
* locations:
* type: array
* description: A list of all available locations for the event type
+1 -1
View File
@@ -13,7 +13,7 @@ import { Meta } from "@storybook/addon-docs";
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
Figma
</a>{" "}
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
the design team.
</p>
</div>
+8 -8
View File
@@ -248,10 +248,10 @@
--cal-bg-inverted: #f3f4f6;
/* background -> components*/
--cal-bg-info: #dee9fc;
--cal-bg-success: #e2fbe8;
--cal-bg-attention: #fceed8;
--cal-bg-error: #f9e3e2;
--cal-bg-info: #263fa9;
--cal-bg-success: #306339;
--cal-bg-attention: #8e3b1f;
--cal-bg-error: #8c2822;
--cal-bg-dark-error: #752522;
/* Borders */
@@ -269,10 +269,10 @@
--cal-text-inverted: #101010;
/* Content/Text -> components */
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321b;
--cal-text-error: #752522;
--cal-text-info: #dee9fc;
--cal-text-success: #e2fbe8;
--cal-text-attention: #fceed8;
--cal-text-error: #f9e3e2;
/* Brand shenanigans
-> These will be computed for the users theme at runtime.
+1 -2
View File
@@ -1,2 +1 @@
public/embed
*.test.ts
public/embed
+8 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
@@ -80,7 +81,13 @@ export default function AppListCard(props: AppListCardProps) {
return (
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
<div className="flex items-center gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
{logo ? (
<img
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
src={logo}
alt={`${title} logo`}
/>
) : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
@@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
type={item.integration.type}
isChecked={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
credentialId={cal.credentialId}
/>
))}
</ul>
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, TextField } from "@calcom/ui";
export default function TwoFactor({ center = true }) {
const { t } = useLocale();
const methods = useFormContext();
return (
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4">{t("backup_code")}</Label>
<p className="text-subtle mb-4 text-sm">{t("backup_code_instructions")}</p>
<TextField
id="backup-code"
label=""
defaultValue=""
placeholder="XXXXX-XXXXX"
minLength={10} // without dash
maxLength={11} // with dash
required
{...methods.register("backupCode")}
/>
</div>
);
}
+2 -2
View File
@@ -5,7 +5,7 @@ import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Input } from "@calcom/ui";
export default function TwoFactor({ center = true }) {
export default function TwoFactor({ center = true, autoFocus = true }) {
const [value, onChange] = useState("");
const { t } = useLocale();
const methods = useFormContext();
@@ -40,7 +40,7 @@ export default function TwoFactor({ center = true }) {
name={`2fa${index + 1}`}
inputMode="decimal"
{...digit}
autoFocus={index === 0}
autoFocus={autoFocus && index === 0}
autoComplete="one-time-code"
/>
))}
@@ -382,24 +382,22 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}}
/>
{selectedLocation && LocationOptions}
<DialogFooter>
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
setEditingLocationType?.("");
locationFormMethods.unregister(["locationType", "locationLink"]);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<DialogFooter className="mt-4">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
setEditingLocationType?.("");
locationFormMethods.unregister(["locationType", "locationLink"]);
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="update-location" type="submit">
{t("update")}
</Button>
</div>
<Button data-testid="update-location" type="submit">
{t("update")}
</Button>
</DialogFooter>
</Form>
</div>
@@ -41,7 +41,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Clock className="m-auto h-6 w-6" />
@@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
/>
</div>
</div>
)}
/>
@@ -13,7 +13,6 @@ import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import cx from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -302,13 +301,7 @@ export const EventSetupTab = (
<div className="flex items-center">
<img
src={eventLocationType.iconUrl}
className={cx(
"h-4 w-4",
// invert all the icons except app icons
eventLocationType.iconUrl &&
!eventLocationType.iconUrl.startsWith("/app-store") &&
"dark:invert"
)}
className="h-4 w-4 dark:invert-[.65]"
alt={`${eventLocationType.label} logo`}
/>
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${
@@ -9,7 +9,6 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select } from "@calcom/ui";
@@ -18,13 +17,14 @@ interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
avatar: string;
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IUserToValue, pendingString: string) => ({
const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
value: `${id || ""}`,
label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
avatar,
email,
});
@@ -66,6 +66,7 @@ type Props = {
formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
};
function getNavigation(props: {
@@ -133,6 +134,7 @@ function EventTypeSingleLayout({
isUpdateMutationLoading,
formMethods,
availability,
isUserOrganizationAdmin,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -142,7 +144,8 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
eventType.schedulingType === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -53,6 +53,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<ul className="p-4">
{calendars?.map((calendar, i) => (
<CalendarSwitch
credentialId={calendar.credentialId}
key={calendar.externalId}
externalId={calendar.externalId}
title={calendar.name || "Nameless Calendar"}
@@ -3,12 +3,13 @@ import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
type FormData = {
@@ -99,11 +100,11 @@ const UserProfile = () => {
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<Avatar
<OrganizationAvatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
organizationSlug={user.organization?.slug}
/>
)}
<input
@@ -5,6 +5,7 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField } from "@calcom/ui";
import BackupCode from "@components/auth/BackupCode";
import TwoFactor from "@components/auth/TwoFactor";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
@@ -20,6 +21,7 @@ interface DisableTwoFactorAuthModalProps {
}
interface DisableTwoFactorValues {
backupCode: string;
totpCode: string;
password: string;
}
@@ -33,11 +35,19 @@ const DisableTwoFactorAuthModal = ({
}: DisableTwoFactorAuthModalProps) => {
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
const { t } = useLocale();
const form = useForm<DisableTwoFactorValues>();
async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
const resetForm = (clearPassword = true) => {
if (clearPassword) form.setValue("password", "");
form.setValue("backupCode", "");
form.setValue("totpCode", "");
setErrorMessage(null);
};
async function handleDisable({ password, totpCode, backupCode }: DisableTwoFactorValues) {
if (isDisabling) {
return;
}
@@ -45,8 +55,10 @@ const DisableTwoFactorAuthModal = ({
setErrorMessage(null);
try {
const response = await TwoFactorAuthAPI.disable(password, totpCode);
const response = await TwoFactorAuthAPI.disable(password, totpCode, backupCode);
if (response.status === 200) {
setTwoFactorLostAccess(false);
resetForm();
onDisable();
return;
}
@@ -54,12 +66,14 @@ const DisableTwoFactorAuthModal = ({
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage(t("incorrect_password"));
}
if (body.error === ErrorCode.SecondFactorRequired) {
} else if (body.error === ErrorCode.SecondFactorRequired) {
setErrorMessage(t("2fa_required"));
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
} else if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage(t("incorrect_2fa"));
} else if (body.error === ErrorCode.IncorrectBackupCode) {
setErrorMessage(t("incorrect_backup_code"));
} else if (body.error === ErrorCode.MissingBackupCodes) {
setErrorMessage(t("missing_backup_codes"));
} else {
setErrorMessage(t("something_went_wrong"));
}
@@ -78,6 +92,7 @@ const DisableTwoFactorAuthModal = ({
<div className="mb-8">
{!disablePassword && (
<PasswordField
required
labelProps={{
className: "block text-sm font-medium text-default",
}}
@@ -85,12 +100,25 @@ const DisableTwoFactorAuthModal = ({
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
)}
<TwoFactor center={false} />
{twoFactorLostAccess ? (
<BackupCode center={false} />
) : (
<TwoFactor center={false} autoFocus={false} />
)}
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
</div>
<DialogFooter showDivider className="relative mt-5">
<Button
color="minimal"
className="mr-auto"
onClick={() => {
setTwoFactorLostAccess(!twoFactorLostAccess);
resetForm(false);
}}>
{twoFactorLostAccess ? t("go_back") : t("lost_access")}
</Button>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, DialogFooter, Form, TextField } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField, showToast } from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
@@ -28,6 +28,7 @@ interface EnableTwoFactorModalProps {
enum SetupStep {
ConfirmPassword,
DisplayBackupCodes,
DisplayQrCode,
EnterTotpCode,
}
@@ -54,16 +55,25 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
[SetupStep.DisplayBackupCodes]: t("backup_code_instructions"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
const [backupCodes, setBackupCodes] = useState([]);
const [backupCodesUrl, setBackupCodesUrl] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const resetState = () => {
setPassword("");
setErrorMessage(null);
setStep(SetupStep.ConfirmPassword);
};
async function handleSetup(e: React.FormEvent) {
e.preventDefault();
@@ -79,6 +89,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
setBackupCodes(body.backupCodes);
// create backup codes download url
const textBlob = new Blob([body.backupCodes.map(formatBackupCode).join("\n")], {
type: "text/plain",
});
if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl);
setBackupCodesUrl(URL.createObjectURL(textBlob));
setDataUri(body.dataUri);
setSecret(body.secret);
setStep(SetupStep.DisplayQrCode);
@@ -113,7 +132,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
const body = await response.json();
if (response.status === 200) {
onEnable();
setStep(SetupStep.DisplayBackupCodes);
return;
}
@@ -141,13 +160,18 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
}
}, [form, handleEnableRef, totpCode]);
const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
<DialogContent
title={step === SetupStep.DisplayBackupCodes ? t("backup_codes") : t("enable_2fa")}
description={setupDescriptions[step]}
type="creation">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<TextField
<PasswordField
label={t("password")}
type="password"
name="password"
@@ -173,6 +197,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
</p>
</>
</WithStep>
<WithStep step={SetupStep.DisplayBackupCodes} current={step}>
<>
<div className="mt-5 grid grid-cols-2 gap-1 text-center font-mono md:pl-10 md:pr-10">
{backupCodes.map((code) => (
<div key={code}>{formatBackupCode(code)}</div>
))}
</div>
</>
</WithStep>
<Form handleSubmit={handleEnable} form={form}>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<div className="-mt-4 pb-2">
@@ -186,9 +219,16 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
</div>
</WithStep>
<DialogFooter className="mt-8" showDivider>
<Button color="secondary" onClick={onCancel}>
{t("cancel")}
</Button>
{step !== SetupStep.DisplayBackupCodes ? (
<Button
color="secondary"
onClick={() => {
onCancel();
resetState();
}}>
{t("cancel")}
</Button>
) : null}
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
@@ -218,6 +258,35 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
{t("enable")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayBackupCodes} current={step}>
<>
<Button
color="secondary"
data-testid="backup-codes-close"
onClick={(e) => {
e.preventDefault();
resetState();
onEnable();
}}>
{t("close")}
</Button>
<Button
color="secondary"
data-testid="backup-codes-copy"
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(backupCodes.map(formatBackupCode).join("\n"));
showToast(t("backup_codes_copied"), "success");
}}>
{t("copy")}
</Button>
<a download="cal-backup-codes.txt" href={backupCodesUrl}>
<Button color="primary" data-testid="backup-codes-download">
{t("download")}
</Button>
</a>
</>
</WithStep>
</DialogFooter>
</Form>
</DialogContent>
@@ -19,10 +19,10 @@ const TwoFactorAuthAPI = {
});
},
async disable(password: string, code: string) {
async disable(password: string, code: string, backupCode: string) {
return fetch("/api/auth/two-factor/totp/disable", {
method: "POST",
body: JSON.stringify({ password, code }),
body: JSON.stringify({ password, code, backupCode }),
headers: {
"Content-Type": "application/json",
},
+9 -27
View File
@@ -57,12 +57,10 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
}),
});
const formMethods = useForm<{
username: string;
email_address: string;
full_name: string;
password: string;
}>({
type formSchemaType = z.infer<typeof formSchema>;
const formMethods = useForm<formSchemaType>({
mode: "onChange",
resolver: zodResolver(formSchema),
});
@@ -70,7 +68,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
props.onError();
};
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
const onSubmit = formMethods.handleSubmit(async (data) => {
props.onSubmit();
const response = await fetch("/api/auth/setup", {
method: "POST",
@@ -130,11 +128,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
className={classNames("my-0", longWebsiteUrl && "rounded-t-none")}
onBlur={onBlur}
name="username"
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("username", e.target.value);
await formMethods.trigger("username");
}}
onChange={(e) => onChange(e.target.value)}
/>
</>
)}
@@ -148,11 +142,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<TextField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("full_name", e.target.value);
await formMethods.trigger("full_name");
}}
onChange={(e) => onChange(e.target.value)}
color={formMethods.formState.errors.full_name ? "warn" : ""}
type="text"
name="full_name"
@@ -172,11 +162,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<EmailField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("email_address", e.target.value);
await formMethods.trigger("email_address");
}}
onChange={(e) => onChange(e.target.value)}
className="my-0"
name="email_address"
/>
@@ -191,11 +177,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<PasswordField
value={value || ""}
onBlur={onBlur}
onChange={async (e) => {
onChange(e.target.value);
formMethods.setValue("password", e.target.value);
await formMethods.trigger("password");
}}
onChange={(e) => onChange(e.target.value)}
hintErrors={["caplow", "admin_min", "num"]}
name="password"
className="my-0"
+8 -10
View File
@@ -4,7 +4,6 @@ import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
import { classNames } from "@calcom/lib";
import cx from "@calcom/lib/classNames";
import { Select } from "@calcom/ui";
export type LocationOption = {
@@ -23,14 +22,7 @@ export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
<div className="flex items-center gap-3">
{icon && (
<img
src={icon}
alt="cover"
// invert all the icons except app icons
className={cx("h-3.5 w-3.5", icon && !icon.startsWith("/app-store") && "dark:invert")}
/>
)}
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:invert-[.65]" />}
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
@@ -57,7 +49,13 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
{e.icon && (
<img
src={e.icon}
alt="app-icon"
className={classNames(e.icon.includes("-dark") && "dark:invert", "h-5 w-5")}
/>
)}
<span>{e.label}</span>
</div>
)}
+1 -14
View File
@@ -14,7 +14,6 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { trpc } from "@calcom/trpc/react";
import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
@@ -222,19 +221,7 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
function useOrgBrandingValues() {
const session = useSession();
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
// Only fetch if we have a session to avoid flooding logs with errors
enabled: session.status === "authenticated",
});
if (res.status === "loading") {
return undefined;
}
if (res.status === "error") return null;
return res.data;
return session?.data?.user.org;
}
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
+1 -1
View File
@@ -32,7 +32,7 @@ function getCspPolicy(nonce: string) {
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
} app.cal.com;
font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
img-src 'self' ${WEBAPP_URL} https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self'
`;
}
-11
View File
@@ -1,11 +0,0 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};
+47 -33
View File
@@ -14,7 +14,8 @@ const {
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
const isOrganizationsEnabled =
process.env.ORGANIZATIONS_ENABLED === "1" || process.env.ORGANIZATIONS_ENABLED === "true";
// To be able to use the version in the app without having to import package.json
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
@@ -226,7 +227,7 @@ const nextConfig = {
async rewrites() {
const beforeFiles = [
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
...(process.env.ORGANIZATIONS_ENABLED
...(isOrganizationsEnabled
? [
{
...matcherConfigRootPath,
@@ -253,6 +254,10 @@ const nextConfig = {
source: "/org/:slug",
destination: "/team/:slug",
},
{
source: "/org/:orgSlug/avatar.png",
destination: "/api/user/avatar?orgSlug=:orgSlug",
},
{
source: "/team/:teamname/avatar.png",
destination: "/api/user/avatar?teamname=:teamname",
@@ -333,44 +338,46 @@ const nextConfig = {
},
],
},
...[
{
...matcherConfigRootPath,
headers: [
...(isOrganizationsEnabled
? [
{
key: "X-Cal-Org-path",
value: "/team/:orgSlug",
...matcherConfigRootPath,
headers: [
{
key: "X-Cal-Org-path",
value: "/team/:orgSlug",
},
],
},
],
},
{
...matcherConfigUserRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user",
...matcherConfigUserRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user",
},
],
},
],
},
{
...matcherConfigUserTypeRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type",
...matcherConfigUserTypeRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type",
},
],
},
],
},
{
...matcherConfigUserTypeEmbedRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type/embed",
...matcherConfigUserTypeEmbedRoute,
headers: [
{
key: "X-Cal-Org-path",
value: "/org/:orgSlug/:user/:type/embed",
},
],
},
],
},
],
]
: []),
];
},
async redirects() {
@@ -447,6 +454,13 @@ const nextConfig = {
},
{
source: "/support",
missing: [
{
type: "header",
key: "host",
value: orgHostPath,
},
],
destination: "/event-types?openIntercom=true",
permanent: true,
},
@@ -463,7 +477,7 @@ const nextConfig = {
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
process.env.ORGANIZATIONS_ENABLED &&
isOrganizationsEnabled &&
// Prevent infinite redirect by checking that we aren't already on localhost
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
? [
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.6",
"version": "3.3.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -129,7 +129,7 @@
"turndown": "^7.1.1",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.20.2"
"zod": "^3.22.2"
},
"devDependencies": {
"@babel/core": "^7.19.6",
+10 -2
View File
@@ -11,6 +11,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
@@ -25,7 +26,7 @@ import prisma from "@calcom/prisma";
import type { EventType, User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -96,7 +97,12 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
<OrganizationAvatar
imageSrc={profile.image}
size="xl"
alt={profile.name}
organizationSlug={profile.organizationSlug}
/>
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
{profile.name}
{user.verified && (
@@ -218,6 +224,7 @@ export type UserPageProps = {
theme: string | null;
brandColor: string;
darkBrandColor: string;
organizationSlug: string | null;
allowSEOIndexing: boolean;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
@@ -321,6 +328,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
organizationSlug: user.organization?.slug ?? null,
allowSEOIndexing: user.allowSEOIndexing ?? true,
};
+1 -11
View File
@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
@@ -11,18 +10,9 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const signupSchema = z.object({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
token: z.string().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).end();
@@ -43,8 +43,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
}
// if user has 2fa
if (user.twoFactorEnabled) {
// if user has 2fa and using backup code
if (user.twoFactorEnabled && req.body.backupCode) {
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error(ErrorCode.InternalServerError);
}
if (!user.backupCodes) {
return res.status(400).json({ error: ErrorCode.MissingBackupCodes });
}
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", ""));
if (index === -1) {
return res.status(400).json({ error: ErrorCode.IncorrectBackupCode });
}
// we delete all stored backup codes at the end, no need to do this here
// if user has 2fa and NOT using backup code, try totp
} else if (user.twoFactorEnabled) {
if (!req.body.code) {
return res.status(400).json({ error: ErrorCode.SecondFactorRequired });
// throw new Error(ErrorCode.SecondFactorRequired);
@@ -82,6 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: session.user.id,
},
data: {
backupCodes: null,
twoFactorEnabled: false,
twoFactorSecret: null,
},
@@ -1,3 +1,4 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
@@ -56,11 +57,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// bytes without updating the sanity checks in the enable and login endpoints.
const secret = authenticator.generateSecret(20);
// generate backup codes with 10 character length
const backupCodes = Array.from(Array(10), () => crypto.randomBytes(5).toString("hex"));
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), process.env.CALENDSO_ENCRYPTION_KEY),
twoFactorEnabled: false,
twoFactorSecret: symmetricEncrypt(secret, process.env.CALENDSO_ENCRYPTION_KEY),
},
@@ -70,5 +75,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const keyUri = authenticator.keyuri(name, "Cal", secret);
const dataUri = await qrcode.toDataURL(keyUri);
return res.json({ secret, keyUri, dataUri });
return res.json({ secret, keyUri, dataUri, backupCodes });
}
+7 -2
View File
@@ -5,10 +5,12 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import notEmpty from "@calcom/lib/notEmpty";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
const selectedCalendarSelectSchema = z.object({
integration: z.string(),
externalId: z.string(),
credentialId: z.number().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -24,7 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: session.user.id,
},
select: {
credentials: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
timeZone: true,
id: true,
selectedCalendars: true,
@@ -37,7 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { credentials, ...user } = userWithCredentials;
if (req.method === "POST") {
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
await prisma.selectedCalendar.upsert({
where: {
userId_integration_externalId: {
@@ -50,6 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: user.id,
integration,
externalId,
credentialId,
},
// already exists
update: {},
+2 -2
View File
@@ -104,7 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const attendeesList = await Promise.all(attendeesListPromises);
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
@@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
};
await sendOrganizerRequestReminderEmail(evt);
@@ -0,0 +1 @@
export { default } from "@calcom/features/webhooks/lib/cron";
+1 -1
View File
@@ -187,7 +187,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400");
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
} catch (error) {
res.statusCode = 404;
+23 -11
View File
@@ -1,17 +1,16 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
orgSlug: z.string(),
/**
* Allow fetching avatar of a particular organization
* Avatars being public, we need not worry about others accessing it.
@@ -21,7 +20,7 @@ const querySchema = z
.partial();
async function getIdentityData(req: NextApiRequest) {
const { username, teamname, orgId } = querySchema.parse(req.query);
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
const org = isValidOrgDomain ? currentOrgDomain : null;
@@ -61,7 +60,23 @@ async function getIdentityData(req: NextApiRequest) {
org,
name: teamname,
email: null,
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
avatar: getPlaceholderAvatar(team?.logo, teamname),
};
}
if (orgSlug) {
const org = await prisma.team.findFirst({
where: getSlugOrRequestedSlug(orgSlug),
select: {
slug: true,
logo: true,
name: true,
},
});
return {
org: org?.slug,
name: org?.name,
email: null,
avatar: getPlaceholderAvatar(org?.logo, org?.name),
};
}
}
@@ -69,18 +84,15 @@ async function getIdentityData(req: NextApiRequest) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// We cache for one day
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
.createHash("md5")
.update(identity?.email || "guest@example.com")
.digest("hex"),
}),
Location: AVATAR_FALLBACK,
});
return res.end();
+9 -1
View File
@@ -12,7 +12,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { HorizontalTabItemProps } from "@calcom/ui";
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
import {
AllApps,
AppStoreCategories,
HorizontalTabs,
TextField,
PopularAppsSlider,
RecentAppsSlider,
} from "@calcom/ui";
import { Search } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@@ -81,6 +88,7 @@ export default function Apps({
<>
<AppStoreCategories categories={categories} />
<PopularAppsSlider items={appStore} />
<RecentAppsSlider items={appStore} />
</>
)}
<AllApps
+36 -12
View File
@@ -21,7 +21,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { ArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft, Lock } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { WithNonceProps } from "@lib/withNonce";
@@ -29,6 +29,7 @@ import withNonce from "@lib/withNonce";
import AddToHomescreen from "@components/AddToHomescreen";
import PageWrapper from "@components/PageWrapper";
import BackupCode from "@components/auth/BackupCode";
import TwoFactor from "@components/auth/TwoFactor";
import AuthContainer from "@components/ui/AuthContainer";
@@ -39,6 +40,7 @@ interface LoginValues {
email: string;
password: string;
totpCode: string;
backupCode: string;
csrfToken: string;
}
export default function Login({
@@ -65,6 +67,7 @@ export default function Login({
const methods = useForm<LoginValues>({ resolver: zodResolver(formSchema) });
const { register, formState } = methods;
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const errorMessages: { [key: string]: string } = {
@@ -98,15 +101,35 @@ export default function Login({
);
const TwoFactorFooter = (
<Button
onClick={() => {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}}
StartIcon={ArrowLeft}
color="minimal">
{t("go_back")}
</Button>
<>
<Button
onClick={() => {
if (twoFactorLostAccess) {
setTwoFactorLostAccess(false);
methods.setValue("backupCode", "");
} else {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}
setErrorMessage(null);
}}
StartIcon={ArrowLeft}
color="minimal">
{t("go_back")}
</Button>
{!twoFactorLostAccess ? (
<Button
onClick={() => {
setTwoFactorLostAccess(true);
setErrorMessage(null);
methods.setValue("totpCode", "");
}}
StartIcon={Lock}
color="minimal">
{t("lost_access")}
</Button>
) : null}
</>
);
const ExternalTotpFooter = (
@@ -130,8 +153,9 @@ export default function Login({
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
@@ -194,7 +218,7 @@ export default function Login({
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{twoFactorRequired ? !twoFactorLostAccess ? <TwoFactor center /> : <BackupCode center /> : null}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
+3 -8
View File
@@ -7,13 +7,8 @@ import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/li
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
isSAMLLoginEnabled,
samlProductID,
samlTenantID,
samlTenantProduct,
} from "@calcom/features/ee/sso/lib/saml";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
@@ -111,7 +106,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
error = "Email not provided";
} else {
try {
const ret = await samlTenantProduct(prisma, emailParam);
const ret = await ssoTenantProduct(prisma, emailParam);
tenant = ret.tenant;
product = ret.product;
} catch (e) {
+103 -38
View File
@@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@@ -17,11 +18,6 @@ import {
ConfirmationDialogContent,
Dialog,
DialogTrigger,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Label,
showToast,
@@ -32,7 +28,7 @@ import {
Tooltip,
VerticalDivider,
} from "@calcom/ui";
import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon";
import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
@@ -95,7 +91,7 @@ export default function Availability() {
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [openSidebar, setOpenSidebar] = useState(false);
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{
@@ -225,33 +221,60 @@ export default function Availability() {
</ConfirmationDialogContent>
</Dialog>
<VerticalDivider className="hidden sm:inline" />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="sm:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownItem
type="button"
color="destructive"
StartIcon={Trash}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
</DropdownItem>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
loadingText={t("delete")}
onConfirm={() => {
schedule !== undefined && deleteMutation.mutate({ scheduleId: schedule.id });
}}>
{t("delete_schedule_description")}
</ConfirmationDialogContent>
</Dialog>
<DropdownMenuSeparator />
<div className="flex h-9 flex-row items-center justify-between px-4 py-2 hover:bg-gray-100">
<div
className={classNames(
openSidebar
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
: ""
)}>
<div
className={classNames(
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
)}>
<div className="flex flex-row items-center pt-5">
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
<p className="-ml-2">{t("availability_settings")}</p>
<Dialog>
<DialogTrigger asChild>
<Button
StartIcon={Trash}
variant="icon"
color="destructive"
aria-label={t("delete")}
className="ml-16 inline"
disabled={schedule?.isLastSchedule}
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
/>
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
loadingText={t("delete")}
onConfirm={() => {
scheduleId && deleteMutation.mutate({ scheduleId });
setOpenSidebar(false);
}}>
{t("delete_schedule_description")}
</ConfirmationDialogContent>
</Dialog>
</div>
<div className="flex flex-col px-2 py-2">
<Skeleton as={Label}>{t("name")}</Skeleton>
<Controller
control={form.control}
name="name"
render={({ field }) => (
<input
className="hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis focus:ring-brand-default disabled:bg-subtle disabled:hover:border-subtle mb-2 block h-9 w-full rounded-md border px-3 py-2 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 disabled:cursor-not-allowed"
{...field}
/>
)}
/>
</div>
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -267,9 +290,44 @@ export default function Availability() {
}}
/>
</div>
</DropdownMenuContent>
</Dropdown>
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
{t("timezone")}
</Skeleton>
<Controller
control={form.control}
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="mt-1 w-72" />
)
}
/>
</div>
<hr className="border-subtle my-7" />
<div className="rounded-md md:block">
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
{t("something_doesnt_look_right")}
</Skeleton>
<div className="mt-3 flex">
<Skeleton as={Button} href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Skeleton>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="border-default border-l-2" />
<Button
className="ml-4 lg:ml-0"
@@ -278,6 +336,13 @@ export default function Availability() {
loading={updateMutation.isLoading}>
{t("save")}
</Button>
<Button
className="ml-3 sm:hidden"
StartIcon={MoreVertical}
variant="icon"
color="secondary"
onClick={() => setOpenSidebar(true)}
/>
</div>
}>
<div className="mt-4 w-full md:mt-0">
@@ -313,7 +378,7 @@ export default function Availability() {
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
@@ -335,7 +400,7 @@ export default function Availability() {
/>
</div>
<hr className="border-subtle my-6 mr-8" />
<div className="hidden rounded-md md:block">
<div className="rounded-md">
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
{t("something_doesnt_look_right")}
</Skeleton>
+3 -1
View File
@@ -461,7 +461,7 @@ export default function Success(props: SuccessProps) {
{locationToDisplay && !isCancelled && (
<>
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">
<div className="col-span-2 mt-3" data-testid="where">
{locationToDisplay.startsWith("http") ? (
<a
href={locationToDisplay}
@@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => {
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodStartDate: true,
periodEndDate: true,
},
@@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async (
eventType: {
seatsPerTimeSlot?: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
[x: string | number | symbol]: unknown;
},
bookingInfo: Partial<
+18 -9
View File
@@ -113,6 +113,7 @@ export type FormValues = {
periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
@@ -183,12 +184,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
created: true,
}))
);
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
}),
"success"
);
showToast(t("event_type_updated_successfully"), "success");
},
async onSettled() {
await utils.viewer.eventTypes.get.invalidate();
@@ -365,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -381,9 +378,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookerLayouts,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -401,7 +401,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -415,6 +415,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -427,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
children,
@@ -448,7 +450,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
currentUserMembership={currentUserMembership}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
<Form
form={formMethods}
id="event-type-form"
@@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -471,9 +475,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -491,7 +498,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -500,6 +507,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -512,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
});
+74 -34
View File
@@ -52,10 +52,9 @@ import {
Skeleton,
Switch,
Tooltip,
ArrowButton,
} from "@calcom/ui";
import {
ArrowDown,
ArrowUp,
Clipboard,
Code,
Copy,
@@ -109,8 +108,10 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const orgBranding = useOrgBranding();
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
avatar: item.profile.image ?? `${orgBranding?.fullDomain ?? WEBAPP_URL}/${item.profile.slug}/avatar.png`,
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
avatar: orgBranding
? `${orgBranding.fullDomain}${item.teamId ? "/team" : ""}/${item.profile.slug}/avatar.png`
: item.profile.image ?? `${WEBAPP_URL + (item.teamId && "/team")}/${item.profile.slug}/avatar.png`,
}));
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
@@ -118,13 +119,15 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
return (
<div>
<HorizontalTabs tabs={tabs} />
{events.length && (
{events.length > 0 ? (
<EventTypeList
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
readOnly={events[0].metadata.readOnly}
/>
) : (
<CreateFirstEventTypeView />
)}
</div>
);
@@ -359,6 +362,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
if (!types.length) {
return group.teamId ? <EmptyEventTypeList group={group} /> : <CreateFirstEventTypeView />;
}
const firstItem = types[0];
const lastItem = types[types.length - 1];
const isManagedEventPrefix = () => {
@@ -378,19 +385,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="hover:bg-muted flex w-full items-center justify-between">
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
<button
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}>
<ArrowUp className="h-5 w-5" />
</button>
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
)}
{!(lastItem && lastItem.id === type.id) && (
<button
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}>
<ArrowDown className="h-5 w-5" />
</button>
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
@@ -400,23 +399,27 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)}
items={
type?.users
? type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)
: []
}
/>
)}
{isManagedEventType && (
{isManagedEventType && type?.children && type.children?.length > 0 && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.children
items={type?.children
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
@@ -671,6 +674,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
title={t(`delete${isManagedEventPrefix()}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
isLoading={deleteMutation.isLoading}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
@@ -711,12 +715,17 @@ const EventTypeListHeading = ({
},
});
const bookerUrl = useBookerUrl();
return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${orgBranding?.fullDomain ?? WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
size="md"
className="mt-1 inline-flex justify-center"
/>
@@ -763,6 +772,12 @@ const CreateFirstEventTypeView = () => {
Icon={LinkIcon}
headline={t("new_event_type_heading")}
description={t("new_event_type_description")}
className="mb-16"
buttonRaw={
<Button href="?dialog=new" variant="button">
{t("create")}
</Button>
}
/>
);
};
@@ -830,6 +845,25 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
const { t } = useLocale();
return (
<>
<EmptyScreen
headline={t("team_no_event_types")}
buttonRaw={
<Button
href={`?dialog=new&eventPage=${group.profile.slug}&teamId=${group.teamId}`}
variant="button"
className="mt-5">
{t("create")}
</Button>
}
/>
</>
);
};
const Main = ({
status,
errorMessage,
@@ -863,7 +897,7 @@ const Main = ({
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<div className="mt-4 flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
@@ -871,12 +905,18 @@ const Main = ({
orgSlug={orgBranding?.slug}
/>
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
{group.eventTypes.length ? (
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
) : group.teamId ? (
<EmptyEventTypeList group={group} />
) : (
<CreateFirstEventTypeView />
)}
</div>
))
)}
+1 -1
View File
@@ -57,7 +57,7 @@ export default function InsightsPage() {
return (
<div>
<ShellMain hideHeadingOnMobile>
<ShellMain heading="Insights" subtitle={t("insights_subtitle")}>
<UpgradeTip
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
@@ -182,6 +182,7 @@ const CalendarsView = () => {
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
credentialId={cal.credentialId}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
name={cal.name || "Nameless calendar"}
+11 -2
View File
@@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -14,10 +15,10 @@ import turndown from "@calcom/lib/turndownService";
import { IdentityProvider } from "@calcom/prisma/enums";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import {
Alert,
Avatar,
Button,
Dialog,
DialogClose,
@@ -223,6 +224,7 @@ const ProfileView = () => {
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
isLoading={updateProfileMutation.isLoading}
userOrganization={user.organization}
onSubmit={(values) => {
if (values.email !== user.email && isCALIdentityProvider) {
setTempFormValues(values);
@@ -364,11 +366,13 @@ const ProfileForm = ({
onSubmit,
extraField,
isLoading = false,
userOrganization,
}: {
defaultValues: FormValues;
onSubmit: (values: FormValues) => void;
extraField?: React.ReactNode;
isLoading: boolean;
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
}) => {
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
@@ -406,7 +410,12 @@ const ProfileForm = ({
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
<OrganizationAvatar
alt={formMethods.getValues("username")}
imageSrc={value}
size="lg"
organizationSlug={userOrganization.slug}
/>
<div className="ms-4">
<ImageUploader
target="avatar"
+4 -9
View File
@@ -17,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui";
@@ -25,14 +26,7 @@ import PageWrapper from "@components/PageWrapper";
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
import { ssrInit } from "../server/lib/ssr";
const signupSchema = z.object({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
token: z.string().optional(),
const signupSchema = apiSignupSchema.extend({
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
});
@@ -46,6 +40,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
const { t, i18n } = useLocale();
const flags = useFlagMap();
const methods = useForm<FormValues>({
mode: "onChange",
resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues,
});
@@ -131,7 +126,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
<TextField
addOnLeading={
orgSlug
? getOrgFullDomain(orgSlug, { protocol: true })
? `${getOrgFullDomain(orgSlug, { protocol: true })}/`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
}
{...register("username")}
+4 -6
View File
@@ -153,11 +153,9 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
) : (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("org_no_teams_yet")}
</h2>
<p className="mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-emphasis mb-2 text-3xl">{" " + t("org_no_teams_yet")}</h2>
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
</div>
</div>
</div>
@@ -212,7 +210,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<EventTypes />
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{(!team.hideBookATeamMember || team.isPrivate) && (
{!team.hideBookATeamMember && !team.isPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
+1 -1
View File
@@ -25,7 +25,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
));
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\..*`;
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
+57
View File
@@ -268,3 +268,60 @@ test.describe("prefill", () => {
});
});
});
test.describe("Booking on different layouts", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await page.goto(`/${user.username}`);
});
test("Book on week layout", async ({ page }) => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="toggle-group-item-week_view"]');
await page.click('[data-testid="incrementMonth"]');
await page.locator('[data-testid="calendar-empty-cell"]').nth(0).click();
// Fill what is this meeting about? name email and notes
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.locator('[name="notes"]').fill("Test notes");
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
// expect page to be booking page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test("Book on column layout", async ({ page }) => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="toggle-group-item-column_view"]');
await page.click('[data-testid="incrementMonth"]');
await page.locator('[data-testid="time"]').nth(0).click();
// Fill what is this meeting about? name email and notes
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.locator('[name="notes"]').fill("Test notes");
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
// expect page to be booking page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
});
+2 -41
View File
@@ -1,60 +1,21 @@
import { expect } from "@playwright/test";
import type { Prisma } from "@prisma/client";
import { uuid } from "short-uuid";
import { v4 as uuidv4 } from "uuid";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
createNewSeatedEventType,
selectFirstAvailableTimeSlotNextMonth,
createUserWithSeatedEventAndAttendees,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
const slug = "seats";
const user = await users.create({
eventTypes: [
{
title: "Seated event",
slug,
seatsPerTimeSlot: 10,
requiresConfirmation: true,
length: 30,
disableGuests: true, // should always be true for seated events
},
],
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eventType = user.eventTypes.find((e) => e.slug === slug)!;
return { user, eventType };
}
async function createUserWithSeatedEventAndAttendees(
fixtures: Pick<Fixtures, "users" | "bookings">,
attendees: Prisma.AttendeeCreateManyBookingInput[]
) {
const { user, eventType } = await createUserWithSeatedEvent(fixtures.users);
const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.ACCEPTED,
// startTime with 1 day from now and endTime half hour after
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
attendees: {
createMany: {
data: attendees,
},
},
});
return { user, eventType, booking };
}
test.describe("Booking with Seats", () => {
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
const user = await users.create({ name: "Seated event" });
@@ -64,7 +25,7 @@ test.describe("Booking with Seats", () => {
await page.waitForSelector('[data-testid="event-types"]');
const eventTitle = "My 2-seated event";
await createNewSeatedEventType(page, { eventTitle });
await expect(page.locator(`text=${eventTitle} event type updated successfully`)).toBeVisible();
await expect(page.locator(`text=Event type updated successfully`)).toBeVisible();
});
test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
+15 -7
View File
@@ -142,17 +142,25 @@ test.describe("Event Types tests", () => {
.first()
.getAttribute("href");
/**
* Verify first organizer address
*/
await page.goto(previewLink ?? "");
await selectFirstAvailableTimeSlotNextMonth(page);
for (const location of locationData) {
await page.locator(`span:has-text("${location}")`).click();
}
await page.locator(`span:has-text("${locationData[0]}")`).click();
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator(`[data-testid="where"]`)).toHaveText(locationData[0]);
/**
* Verify second organizer address
*/
await page.goto(previewLink ?? "");
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator(`span:has-text("${locationData[1]}")`).click();
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator(`[data-testid="where"]`)).toHaveText(locationData[1]);
});
test.describe("Different Locations Tests", () => {
@@ -0,0 +1,61 @@
import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
type Route = {
id: string;
action: {
type: string;
value: string;
};
isFallback: boolean;
queryValue: {
id: string;
type: string;
};
};
export const createRoutingFormsFixture = () => {
return {
async create({
userId,
teamId,
name,
fields,
routes = [],
}: {
name: string;
userId: number;
teamId: number | null;
routes?: Route[];
fields: {
type: string;
label: string;
identifier?: string;
required: boolean;
}[];
}) {
return await prisma.app_RoutingForms_Form.create({
data: {
name,
userId,
teamId,
routes: [
...routes,
// Add a fallback route always, this is taken care of tRPC route normally but do it manually while running the query directly.
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: fields.map((f) => ({
id: uuidv4(),
...f,
})),
},
});
},
};
};
+5
View File
@@ -10,6 +10,7 @@ import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings";
import { createEmbedsFixture, createGetActionFiredDetails } from "../fixtures/embeds";
import { createPaymentsFixture } from "../fixtures/payments";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
@@ -23,6 +24,7 @@ export interface Fixtures {
servers: ReturnType<typeof createServersFixture>;
prisma: typeof prisma;
emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
}
declare global {
@@ -71,6 +73,9 @@ export const test = base.extend<Fixtures>({
prisma: async ({}, use) => {
await use(prisma);
},
routingForms: async ({}, use) => {
await use(createRoutingFormsFixture());
},
emails: async ({}, use) => {
if (IS_MAILHOG_ENABLED) {
const mailhogAPI = mailhog();
+46
View File
@@ -6,6 +6,10 @@ import { createServer } from "http";
import { noop } from "lodash";
import type { API, Messages } from "mailhog";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Fixtures } from "./fixtures";
import { test } from "./fixtures";
export function todo(title: string) {
@@ -192,6 +196,7 @@ export async function installAppleCalendar(page: Page) {
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
}
export async function getEmailsReceivedByUser({
emails,
userEmail,
@@ -228,3 +233,44 @@ export async function expectEmailsToHaveSubject({
expect(organizerFirstEmail.subject).toBe(emailSubject);
expect(bookerFirstEmail.subject).toBe(emailSubject);
}
// this method is not used anywhere else
// but I'm keeping it here in case we need in the future
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
const slug = "seats";
const user = await users.create({
eventTypes: [
{
title: "Seated event",
slug,
seatsPerTimeSlot: 10,
requiresConfirmation: true,
length: 30,
disableGuests: true, // should always be true for seated events
},
],
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eventType = user.eventTypes.find((e) => e.slug === slug)!;
return { user, eventType };
}
export async function createUserWithSeatedEventAndAttendees(
fixtures: Pick<Fixtures, "users" | "bookings">,
attendees: Prisma.AttendeeCreateManyBookingInput[]
) {
const { user, eventType } = await createUserWithSeatedEvent(fixtures.users);
const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.ACCEPTED,
// startTime with 1 day from now and endTime half hour after
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
attendees: {
createMany: {
data: attendees,
},
},
});
return { user, eventType, booking };
}
+24 -2
View File
@@ -4,11 +4,14 @@ import { authenticator } from "otplib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import { prisma } from "@calcom/prisma";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
// TODO: add more backup code tests, e.g. login + disabling 2fa with backup
// a test to logout requires both a succesfull login as logout, to prevent
// a doubling of tests failing on logout & logout, we can group them.
test.describe("2FA Tests", async () => {
@@ -45,6 +48,8 @@ test.describe("2FA Tests", async () => {
secret: secret!,
});
// FIXME: this passes even when switch is not checked, compare to test
// below which checks for data-state="checked" and works as expected
await page.waitForSelector(`[data-testid=two-factor-switch]`);
await expect(page.locator(`[data-testid=two-factor-switch]`).isChecked()).toBeTruthy();
@@ -57,7 +62,7 @@ test.describe("2FA Tests", async () => {
await test.step("Login with 2FA enabled", async () => {
await user.login();
const userWith2FaSecret = await prisma?.user.findFirst({
const userWith2FaSecret = await prisma.user.findFirst({
where: {
id: user.id,
},
@@ -103,6 +108,23 @@ test.describe("2FA Tests", async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await fillOtp({ page, secret: secret! });
// backup codes are now showing, so run a few tests
// click download button
const promise = page.waitForEvent("download");
await page.getByTestId("backup-codes-download").click();
const download = await promise;
expect(download.suggestedFilename()).toBe("cal-backup-codes.txt");
// TODO: check file content
// click copy button
await page.getByTestId("backup-codes-copy").click();
await page.getByTestId("toast-success").waitFor();
// TODO: check clipboard content
// close backup code dialog
await page.getByTestId("backup-codes-close").click();
await expect(page.locator(`[data-testid=two-factor-switch][data-state="checked"]`)).toBeVisible();
return user;
@@ -117,7 +139,7 @@ test.describe("2FA Tests", async () => {
await page.click(`[data-testid=two-factor-switch][data-state="checked"]`);
await page.fill('input[name="password"]', userPassword);
const userWith2FaSecret = await prisma?.user.findFirst({
const userWith2FaSecret = await prisma.user.findFirst({
where: {
id: user.id,
},
@@ -135,6 +135,24 @@ test.describe("Manage Booking Questions", () => {
});
});
});
await test.step("Verify that we can prefill name field with no lastname", async () => {
const searchParams = new URLSearchParams();
searchParams.append("name", "FirstName");
await doOnFreshPreviewWithSearchParams(searchParams, page, context, async (page) => {
await selectFirstAvailableTimeSlotNextMonth(page);
await expectSystemFieldsToBeThereOnBookingPage({
page,
isFirstAndLastNameVariant: true,
values: {
name: {
firstName: "FirstName",
lastName: "",
},
},
});
});
});
});
});
@@ -506,6 +524,27 @@ async function doOnFreshPreview(
return previewTabPage;
}
async function doOnFreshPreviewWithSearchParams(
searchParams: URLSearchParams,
page: Page,
context: PlaywrightTestArgs["context"],
callback: (page: Page) => Promise<void>,
persistTab = false
) {
const previewUrl = (await page.locator('[data-testid="preview-button"]').getAttribute("href")) || "";
const previewUrlObj = new URL(previewUrl);
searchParams.forEach((value, key) => {
previewUrlObj.searchParams.append(key, value);
});
const previewTabPage = await context.newPage();
await previewTabPage.goto(previewUrlObj.toString());
await callback(previewTabPage);
if (!persistTab) {
await previewTabPage.close();
}
return previewTabPage;
}
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="toggle-field"]').click();
await saveEventType(page);
+281 -32
View File
@@ -1,4 +1,9 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/client";
import { test } from "./lib/fixtures";
import {
@@ -7,10 +12,37 @@ import {
selectFirstAvailableTimeSlotNextMonth,
waitFor,
gotoRoutingLink,
createUserWithSeatedEventAndAttendees,
} from "./lib/testUtils";
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
test.afterEach(({ users }) => users.deleteAll());
async function createWebhookReceiver(page: Page) {
const webhookReceiver = createHttpServer();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
return webhookReceiver;
}
test.describe("BOOKING_CREATED", async () => {
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
@@ -55,8 +87,6 @@ test.describe("BOOKING_CREATED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body: any = request.body;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -187,8 +217,6 @@ test.describe("BOOKING_REJECTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -246,7 +274,7 @@ test.describe("BOOKING_REJECTED", async () => {
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
destinationCalendar: [],
// hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
@@ -311,8 +339,6 @@ test.describe("BOOKING_REQUESTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -390,55 +416,278 @@ test.describe("BOOKING_REQUESTED", async () => {
});
});
test.describe("FORM_SUBMITTED", async () => {
test("can submit a form and get a submission event", async ({ page, users }) => {
const webhookReceiver = createHttpServer();
test.describe("BOOKING_RESCHEDULED", async () => {
test("can reschedule a booking and get a booking rescheduled event", async ({ page, users, bookings }) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.apiLogin();
await page.goto("/settings/teams/new");
await page.waitForLoadState("networkidle");
const teamName = `${user.username}'s Team`;
// Create a new team
await page.locator('input[name="name"]').fill(teamName);
await page.locator('input[name="slug"]').fill(teamName);
await page.locator('button[type="submit"]').click();
const webhookReceiver = await createWebhookReceiver(page);
await page.locator("text=Publish team").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
const booking = await bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.ACCEPTED,
});
await page.waitForLoadState("networkidle");
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*booking/);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } })!;
expect(newBooking).not.toBeNull();
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
expect(request.body).toMatchObject({
triggerEvent: "BOOKING_RESCHEDULED",
payload: {
uid: newBooking?.uid,
},
});
});
test("when rescheduling to a booking that already exists, should send a booking rescheduled event with the existant booking uid", async ({
page,
users,
bookings,
}) => {
const { user, eventType, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);
await prisma.eventType.update({
where: { id: eventType.id },
data: { requiresConfirmation: false },
});
await user.apiLogin();
const webhookReceiver = await createWebhookReceiver(page);
const bookingAttendees = await prisma.attendee.findMany({
where: { bookingId: booking.id },
select: {
id: true,
email: true,
},
});
const bookingSeats = bookingAttendees.map((attendee) => ({
bookingId: booking.id,
attendeeId: attendee.id,
referenceUid: uuidv4(),
}));
await prisma.bookingSeat.createMany({
data: bookingSeats,
});
const references = await prisma.bookingSeat.findMany({
where: { bookingId: booking.id },
include: { attendee: true },
});
await page.goto(`/reschedule/${references[0].referenceUid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({
where: {
attendees: {
some: {
email: bookingAttendees[0].email,
},
},
},
});
// --- ensuring that new booking was created
expect(newBooking).not.toBeNull();
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [firstRequest] = webhookReceiver.requestList;
expect(firstRequest?.body).toMatchObject({
triggerEvent: "BOOKING_RESCHEDULED",
payload: {
uid: newBooking?.uid,
},
});
await page.goto(`/reschedule/${references[1].referenceUid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*booking/);
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(2);
});
const [_, secondRequest] = webhookReceiver.requestList;
expect(secondRequest?.body).toMatchObject({
triggerEvent: "BOOKING_RESCHEDULED",
payload: {
// in the current implementation, it is the same as the first booking
uid: newBooking?.uid,
},
});
});
});
test.describe("FORM_SUBMITTED", async () => {
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.waitForLoadState("networkidle");
await page.goto(`/settings/developer/webhooks/new`);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([page.click("[type=submit]"), page.goForward()]);
await page.click("[type=submit]");
// Page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
await page.waitForLoadState("networkidle");
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", "TEST FORM");
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: null,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: formId });
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId: null,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
const teamId = await clickFirstTeamWebhookCta(page);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
});
async function clickFirstTeamWebhookCta(page: Page) {
await page.click('[data-testid="new_webhook"]');
await page.click('[data-testid="option-team-1"]');
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
const url = page.url();
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
return teamId;
}
+1
View File
@@ -0,0 +1 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="32" height="32" rx="16" fill="#E5E7EB"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Z" fill="#1F2937"/><path d="M26.667 32a10.667 10.667 0 1 0-21.334 0" fill="#1F2937"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Zm0 0A10.667 10.667 0 0 1 26.667 32M16 21.333A10.666 10.666 0 0 0 5.333 32" stroke="#1F2937" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="a"><rect width="32" height="32" rx="16" fill="#fff"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 635 B

+9 -11
View File
@@ -13,7 +13,7 @@
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"verify_email_subject": "{{appName}}: تأكيد حسابك",
"check_your_email": "تحقق من بريدك الإلكتروني",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان تسليم البريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
"verify_email_email_button": "تأكيد البريد الإلكتروني",
@@ -130,7 +130,7 @@
"upgrade_banner_action": "قم بالترقية هنا",
"team_upgraded_successfully": "تمت ترقية فريقك بنجاح!",
"org_upgrade_banner_description": "شكرًا لك على تجربة خطة Organization الجديدة التي نقدمها. لقد لاحظنا أن منظمتك \"{{teamName}}\" بحاجة إلى الترقية.",
"org_upgraded_successfully": "تمت ترقية منظمتك بنجاح!",
"org_upgraded_successfully": "تمت ترقية خطة Organization بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا،",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
@@ -550,7 +550,7 @@
"team_description": "بضع جمل عن فريقك. ستظهر على صفحة رابط فريقك.",
"org_description": "بضع جمل عن منظمتك. ستظهر على صفحة رابط منظمتك.",
"members": "الأعضاء",
"organization_members": "أعضاء المنظمة",
"organization_members": "أعضاء Organization",
"member": "العضو",
"number_member_one": "{{count}} عضو",
"number_member_other": "{{count}} من الأعضاء",
@@ -1074,11 +1074,11 @@
"event_cancelled": "تم إلغاء هذا الحدث",
"emailed_information_about_cancelled_event": "لقد أرسلنا إليك وإلى الحاضرين الآخرين بريداً إلكترونياً لإعلامهم.",
"this_input_will_shown_booking_this_event": "سيُعرَض هذا المُدخَل عند حجز هذا الحدث",
"meeting_url_in_conformation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
"meeting_url_in_confirmation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
"number_provided": "سيتم توفير رقم الهاتف",
"before_event_trigger": "قبل بدء الحدث",
"event_cancelled_trigger": "متى تم إلغاء هذا الحدث",
"event_cancelled_trigger": "عند إلغاء هذا الحدث",
"new_event_trigger": "متى تم حجز الحدث الجديد",
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
@@ -1652,9 +1652,8 @@
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_user_cta": "عرض الدعوة",
"email_no_user_invite_heading": "تمت دعوتك للانضمام إلى {{appName}} {{entity}}",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading": "لقد دعاك {{invitedBy}} للانضمام إلى {{entity}} `{{teamName}}` على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading_team": "دعاك {{invitedBy}} للانضمام إلى فريقه {{teamName}} على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدول زمني خالٍ من التوتر مع {{entity}} في لمح البصر.",
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
@@ -1834,7 +1833,7 @@
"invite_link_copied": "تم نسخ رابط الدعوة",
"invite_link_deleted": "تم حذف رابط الدعوة",
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
"link_expires_after": "تم تعيين الروابط للانتهاء بعد...",
"link_expires_after": "تم تعيين انتهاء صلاحية الروابط بعد...",
"one_day": "1 يوم",
"seven_days": "7 أيام",
"thirty_days": "30 يومًا",
@@ -1871,7 +1870,6 @@
"first_event_type_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من الأحداث",
"install_app_on": "تثبيت التطبيق على",
"create_for": "إنشاء من أجل",
"setup_organization": "إعداد منظمة",
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة ومهام سير العمل وأنواع الأحداث باستخدام الجدولة الدوارة والجماعية.",
"organization_banner_title": "إدارة المنظمات ذات الفرق المتعددة",
"set_up_your_organization": "إعداد منظمتك",
@@ -1886,7 +1884,7 @@
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
"additional_url_parameters": "معلمات الرابط الإضافية",
"about_your_organization": "حول منظمتك",
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.",
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل المشتركة والمزيد.",
"create_your_teams": "إنشاء فرقك",
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
"invite_organization_admins": "دعوة مشرفي منظمتك",
@@ -1927,7 +1925,7 @@
"404_the_org": "المنظمة",
"404_the_team": "الفريق",
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
"404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي",
"404_claim_entity_team": "انضم لهذا الفريق وابدأ في إدارة الجداول الزمنية بشكل جماعي",
"insights_all_org_filter": "الكل",
"insights_team_filter": "الفريق: {{teamName}}",
"insights_user_filter": "المستخدم: {{userName}}",
+10 -12
View File
@@ -118,7 +118,7 @@
"team_info": "Informace o týmu",
"request_another_invitation_email": "Pokud si nepřejete použít e-mail {{toEmail}} pro {{appName}} nebo už účet na {{appName}} máte, požádejte prosím o další pozvánku na jiný e-mail.",
"you_have_been_invited": "Byli jste pozváni do týmu {{teamName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k subjektu {{entity}} {{team}} v aplikaci {{appName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k {{entity}} {{team}} v aplikaci {{appName}}",
"hidden_team_member_title": "V tomto týmu nejste viditelní",
"hidden_team_member_message": "Vaše místo není zaplacené. Buď přejděte na tarif Pro, nebo informujte vlastníka týmu, že za vaše místo může zaplatit.",
"hidden_team_owner_message": "Pro využívání týmů potřebujete tarif Pro, budete skrytí, než přejdete na vyšší verzi.",
@@ -130,7 +130,7 @@
"upgrade_banner_action": "Upgradovat zde",
"team_upgraded_successfully": "Váš tým byl úspěšně přešel na vyšší verzi!",
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli náš tarif Organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
"org_upgraded_successfully": "Vaše organizace byla upgradována!",
"org_upgraded_successfully": "Váš upgrade na tarif Organization byl úspěšně dokončen!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Zdravíme,",
"forgot_your_password_calcom": "Zapomněli jste heslo? - {{appName}}",
@@ -307,7 +307,7 @@
"layout": "Rozvržení",
"bookerlayout_default_title": "Výchozí zobrazení",
"bookerlayout_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat.",
"bookerlayout_user_settings_title": "Rozvržení rezervace",
"bookerlayout_user_settings_title": "Rozvržení rezervací",
"bookerlayout_user_settings_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat. Lze nadefinovat pro každou událost zvlášť.",
"bookerlayout_month_view": "Měsíční",
"bookerlayout_week_view": "Týdenní",
@@ -403,7 +403,7 @@
"recording_ready": "Odkaz ke stažení záznamu je připraven",
"booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Byla vyžádána rezervace",
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
"meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán",
"event_triggers": "Eventy na základě akce",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Tato událost je zrušena",
"emailed_information_about_cancelled_event": "Poslali jsme vám a ostatním účastníkům informační e-mail.",
"this_input_will_shown_booking_this_event": "Toto pole se zobrazí při rezervaci této události",
"meeting_url_in_conformation_email": "Adresa URL schůzky je v potvrzovacím e-mailu",
"meeting_url_in_confirmation_email": "Adresa URL schůzky je v potvrzovacím e-mailu",
"url_start_with_https": "Adresa URL musí začínat http:// nebo https://",
"number_provided": "Bude uvedeno telefonní číslo",
"before_event_trigger": "před zahájením události",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_user_cta": "Zobrazit pozvánku",
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{entity}} v aplikaci {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu {{entity}} v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu {{entity}} umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_user_invite_subheading_team": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu „{{teamName}}“ v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem {{entity}} užívat plánování bez stresu.",
"email_no_user_step_one": "Vyberte si uživatelské jméno",
"email_no_user_step_two": "Připojte svůj účet kalendáře",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Vytvořte svůj první webhook pro tento typ události",
"install_app_on": "Nainstalovat aplikaci v",
"create_for": "Vytvořit pro",
"setup_organization": "Nastavení organizace",
"organization_banner_description": "Vytvořte prostředí, ve kterém budou moct vaše týmy vytvářet sdílené aplikace, pracovní postupy a typy událostí s kolektivním plánováním nebo plánováním typu round-robin.",
"organization_banner_title": "Správa organizace s více týmy",
"set_up_your_organization": "Nastavení organizace",
@@ -1889,12 +1887,12 @@
"admin_username": "Uživatelské jméno správce",
"organization_name": "Název organizace",
"organization_url": "Adresa URL organizace",
"organization_verify_header": "Ověřte e-mailovou adresu organizace",
"organization_verify_header": "Ověřte svou e-mailovou adresu organizace",
"organization_verify_email_body": "Pomocí uvedeného kódu ověřte vaši e-mailovou adresu, abyste mohli pokračovat v nastavení vaší organizace.",
"additional_url_parameters": "Další parametry adresy URL",
"about_your_organization": "Informace o vaší organizaci",
"about_your_organization_description": "Organizace jsou sdílená prostředí, ve kterých můžete vytvořit několik týmů se sdílenými členy, typy událostí, aplikacemi, pracovními postupy a mnoha dalšími věcmi.",
"create_your_teams": "Vytvoření týmů",
"create_your_teams": "Vytvořit tým",
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování",
"invite_organization_admins": "Pozvěte správce své organizace",
"invite_organization_admins_description": "Tito správci budou mít přístup ke všem týmům ve vaší organizaci. Správce a členy týmů můžete přidat později.",
@@ -1934,11 +1932,11 @@
"404_the_org": "Organizace",
"404_the_team": "Tým",
"404_claim_entity_org": "Vyžádejte si subdoménu pro svou organizaci",
"404_claim_entity_team": "Vyžádejte si tento tým a začněte kolektivně spravovat rozvrhy",
"404_claim_entity_team": "Vyžádejte si přístup k tomuto týmu a začněte kolektivně spravovat rozvrhy",
"insights_all_org_filter": "Všechny aplikace",
"insights_team_filter": "Tým: {{teamName}}",
"insights_user_filter": "Uživatel: {{userName}}",
"insights_subtitle": "Zobrazte si přehled o rezervacích napříč vašimi událostmi",
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
"custom_plan": "Vlastní plán",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -960,7 +960,7 @@
"reschedule_placeholder": "Lad andre vide, hvorfor du har brug for at omlægge",
"emailed_information_about_cancelled_event": "Vi har sendt en e-mail til dig og de andre deltagere for at give dem besked.",
"this_input_will_shown_booking_this_event": "Dette input vil blive vist ved booking af denne begivenhed",
"meeting_url_in_conformation_email": "Møde-url er i bekræftelses-mailen",
"meeting_url_in_confirmation_email": "Møde-url er i bekræftelses-mailen",
"url_start_with_https": "URL'en skal starte med http:// eller https://",
"number_provided": "Telefonnummer vil blive angivet",
"before_event_trigger": "før begivenheden starter",
+11 -8
View File
@@ -136,7 +136,7 @@
"team_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Teamplan getestet haben. Wir haben festgestellt, dass Ihr Team „{{teamName}}“ aktualisiert werden muss.",
"upgrade_banner_action": "Hier aufrüsten",
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Tarif „Organization“ getestet haben. Wir haben festgestellt, dass Ihre Organization „{{teamName}}“ ein Upgrade benötigt.",
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
"hey_there": "Hallo,",
@@ -225,6 +225,7 @@
"create_account": "Konto erstellen",
"confirm_password": "Passwort bestätigen",
"confirm_auth_change": "Dies ändert die Art und Weise, wie Sie sich anmelden",
"confirm_auth_email_change": "Wenn Sie Ihre E-Mail-Adresse ändern, wird Ihre derzeitige Authentifizierungsmethode für die Anmeldung bei Cal.com unterbrochen. Wir werden Sie bitten, Ihre neue E-Mail-Adresse zu verifizieren. In der Folge werden Sie abgemeldet und verwenden Ihre neue E-Mail-Adresse, um sich anstelle Ihrer aktuellen Authentifizierungsmethode anzumelden, nachdem Sie Ihr Passwort anhand der Anweisungen, die Ihnen per E-Mail zugesandt werden, festgelegt haben.",
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
"email_change": "Melden Sie sich mit Ihrer neuen E-Mail-Adresse und Ihrem Passwort wieder an.",
"create_your_account": "Erstellen Sie Ihr Konto",
@@ -255,6 +256,7 @@
"available_apps": "Verfügbare Apps",
"available_apps_lower_case": "Verfügbare Apps",
"available_apps_desc": "Sie haben keine Apps installiert. Sehen Sie sich beliebte Apps unten an und entdecken Sie noch mehr in unserem <1>App Store</1>",
"fixed_host_helper": "Füge jeden hinzu, der an der Veranstaltung teilnehmen muss. <1>Mehr erfahren</1>",
"check_email_reset_password": "Überprüfen Sie Ihre E-Mail. Wir haben Ihnen einen Link zum Zurücksetzen Ihres Passworts gesendet.",
"finish": "Fertig",
"organization_general_description": "Einstellungen für die Sprache und Zeitzone Ihres Teams verwalten",
@@ -527,7 +529,7 @@
"location": "Ort",
"address": "Adresse",
"enter_address": "Geben Sie eine Adresse ein",
"in_person_attendee_address": "In Person (Adresse von Ihnen)",
"in_person_attendee_address": "Vor Ort (Adresse von Ihnen)",
"yes": "Ja",
"no": "Nein",
"additional_notes": "Zusätzliche Notizen",
@@ -537,7 +539,7 @@
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
"in_person_meeting": "Vor-Ort-Termin",
"in_person": "Persönlich (Organisator-Adresse)",
"in_person": "Vor Ort (Organisator-Adresse)",
"link_meeting": "Termin verknüpfen",
"phone_number": "Telefonnummer",
"attendee_phone_number": "Telefonnummer",
@@ -559,6 +561,7 @@
"leave": "Verlassen",
"profile": "Profil",
"my_team_url": "Meine Team-URL",
"my_teams": "Meine Teams",
"team_name": "Teamname",
"your_team_name": "Ihr Teamname",
"team_updated_successfully": "Team erfolgreich aktualisiert",
@@ -1092,7 +1095,7 @@
"event_cancelled": "Dieser Termin ist abgesagt",
"emailed_information_about_cancelled_event": "Wir haben Ihnen und den anderen Teilnehmern eine E-Mail gesendet, damit alle Bescheid wissen.",
"this_input_will_shown_booking_this_event": "Diese Eingabe wird bei der Buchung dieses Termins angezeigt",
"meeting_url_in_conformation_email": "Termin-URL ist in der Bestätigungsmail",
"meeting_url_in_confirmation_email": "Termin-URL ist in der Bestätigungsmail",
"url_start_with_https": "URL muss mit http:// oder https:// beginnen",
"number_provided": "Telefonnummer wird angegeben",
"before_event_trigger": "vor Beginn des Termins",
@@ -1676,10 +1679,9 @@
"delete_sso_configuration_confirmation_description": "Sind Sie sicher, dass Sie die {{connectionType}}-Konfiguration löschen möchten? Ihre Teammitglieder, die sich über {{connectionType}} anmelden, werden keinen Zugriff mehr auf Cal.com haben.",
"organizer_timezone": "Zeitzone des Veranstalters",
"email_user_cta": "Einladung anzeigen",
"email_no_user_invite_heading": "Sie wurden eingeladen, einem {{entity}} in {{appName}} beizutreten",
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem {{entity}} „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem {{entity}} ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrem {{entity}} genießen.",
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrer {{entity}} genießen.",
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
"email_no_user_step_two": "Verbinden Sie Ihr Kalenderkonto",
"email_no_user_step_three": "Legen Sie Ihre Verfügbarkeit fest",
@@ -1899,7 +1901,6 @@
"install_app_on": "App installieren auf",
"create_for": "Erstellen für",
"currency": "Währung",
"setup_organization": "Eine Organization einrichten",
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
"set_up_your_organization": "Ihre Organization einrichten",
@@ -1974,6 +1975,8 @@
"org_team_names_example_5": "z.B. Data Analytics Team",
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
"add_to_team": "Zum Team hinzufügen",
"user_isnt_in_any_teams": "Dieser Benutzer ist in keinem Team",
"kyc_verification_information": "Aus Sicherheitsgründen müssen Sie Ihren {{teamOrAccount}} verifizieren, bevor Sie Textnachrichten an die Teilnehmer senden können. Bitte kontaktieren Sie uns unter <a>{{supportEmail}}</a> und geben Sie folgende Informationen an:",
"org_admin_other_teams": "Weitere Teams",
"no_other_teams_found": "Keine weiteren Teams gefunden",
+38 -11
View File
@@ -266,6 +266,7 @@
"nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.",
"set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.",
"set_availability": "Set your availability",
"availability_settings": "Availability Settings",
"continue_without_calendar": "Continue without calendar",
"connect_your_calendar": "Connect your calendar",
"connect_your_video_app": "Connect your video apps",
@@ -290,9 +291,9 @@
"add_another_calendar": "Add another calendar",
"other": "Other",
"email_sign_in_subject": "Your sign-in link for {{appName}}",
"emailed_you_and_attendees": "We emailed you and the other attendees a calendar invitation with all the details.",
"emailed_you_and_attendees_recurring": "We emailed you and the other attendees a calendar invitation for the first of these recurring events.",
"emailed_you_and_any_other_attendees": "You and any other attendees have been emailed with this information.",
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
"user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.",
@@ -590,6 +591,8 @@
"invite_new_member": "Invite a new team member",
"invite_new_member_description": "Note: This will <1>cost an extra seat ($15/m)</1> on your subscription.",
"invite_new_team_member": "Invite someone to your team.",
"upload_csv_file": "Upload a .csv file",
"invite_via_email": "Invite via email",
"change_member_role": "Change team member role",
"disable_cal_branding": "Disable {{appName}} branding",
"disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.",
@@ -678,8 +681,8 @@
"new_event_type_btn": "New event type",
"new_event_type_heading": "Create your first event type",
"new_event_type_description": "Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.",
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully",
"event_type_updated_successfully": "{{eventTypeTitle}} event type updated successfully",
"event_type_created_successfully": "Event type created successfully",
"event_type_updated_successfully": "Event type updated successfully",
"event_type_deleted_successfully": "Event type deleted successfully",
"hours": "Hours",
"people": "People",
@@ -972,6 +975,8 @@
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
"seats_available_one": "Seat available",
"seats_available_other": "Seats available",
"seats_nearly_full": "Seats almost full",
"seats_half_full": "Seats filling fast",
"number_of_seats": "Number of seats per booking",
"enter_number_of_seats": "Enter number of seats",
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
@@ -1052,12 +1057,15 @@
"how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?",
"choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
"setting_up_make": "Setting up your Make integration",
"generate_api_key": "Generate API key",
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Go to <1><0>Make Invite Link</0></1> and install the Cal.com app.</0><1>Log into your Make account and create a new Scenario.</1><2>Select Cal.com as your Trigger app. Also choose a Trigger event.</2><3>Choose your account and then enter your Unique API Key.</3><4>Test your Trigger.</4><5>You're set!</5>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"install_make_app": "Please first install the Make App in the app store.",
"connect_apple_server": "Connect to Apple Server",
"calendar_url": "Calendar URL",
"apple_server_generate_password": "Generate an app specific password to use with {{appName}} at",
@@ -1095,9 +1103,9 @@
"reschedule_optional": "Reason for rescheduling (optional)",
"reschedule_placeholder": "Let others know why you need to reschedule",
"event_cancelled": "This event is canceled",
"emailed_information_about_cancelled_event": "We emailed you and the other attendees to let them know.",
"emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.",
"this_input_will_shown_booking_this_event": "This input will be shown when booking this event",
"meeting_url_in_conformation_email": "Meeting url is in the confirmation email",
"meeting_url_in_confirmation_email": "Meeting url is in the confirmation email",
"url_start_with_https": "URL needs to start with http:// or https://",
"number_provided": "Phone number will be provided",
"before_event_trigger": "before event starts",
@@ -1107,6 +1115,7 @@
"email_attendee_action": "send email to attendees",
"sms_attendee_action": "Send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"send_reminder_sms": "Easily send meeting reminders via SMS to your attendees",
"whatsapp_number_action": "send WhatsApp message to a specific number",
"whatsapp_attendee_action": "send WhatsApp message to attendee",
"workflows": "Workflows",
@@ -1449,6 +1458,7 @@
"add_limit": "Add Limit",
"team_name_required": "Team name required",
"show_attendees": "Share attendee information between guests",
"show_available_seats_count": "Show the number of available seats",
"how_booking_questions_as_variables": "How to use booking questions as variables?",
"format": "Format",
"uppercase_for_letters": "Use uppercase for all letters",
@@ -1682,9 +1692,11 @@
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"organizer_timezone": "Organizer timezone",
"email_user_cta": "View Invitation",
"email_no_user_invite_heading": "You’ve been invited to join a {{appName}} {{entity}}",
"email_no_user_invite_heading_team": "You’ve been invited to join a {{appName}} team",
"email_no_user_invite_heading_org": "You’ve been invited to join a {{appName}} organization",
"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 {{entity}} `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your {{entity}} to schedule meetings without the email tennis.",
"email_user_invite_subheading_team": "{{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_user_invite_subheading_org": "{{invitedBy}} has invited you to join their organization `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your organization 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 {{entity}} in no time.",
"email_no_user_step_one": "Choose your username",
"email_no_user_step_two": "Connect your calendar account",
@@ -1907,7 +1919,6 @@
"install_app_on": "Install app on",
"create_for": "Create for",
"currency": "Currency",
"setup_organization": "Setup an Organization",
"organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.",
"organization_banner_title": "Manage organizations with multiple teams",
"set_up_your_organization": "Set up your organization",
@@ -2010,6 +2021,13 @@
"member_removed": "Member removed",
"my_availability": "My Availability",
"team_availability": "Team Availability",
"backup_code": "Backup Code",
"backup_codes": "Backup Codes",
"backup_code_instructions": "Each backup code can be used exactly once to grant access without your authenticator.",
"backup_codes_copied": "Backup codes copied!",
"incorrect_backup_code": "Backup code is incorrect.",
"lost_access": "Lost access",
"missing_backup_codes": "No backup codes found. Please generate them in your settings.",
"admin_org_notification_email_subject": "New organization created: pending action",
"hi_admin": "Hi Administrator",
"admin_org_notification_email_title": "An organization requires DNS setup",
@@ -2025,6 +2043,15 @@
"mark_dns_configured": "Mark as DNS configured",
"value": "Value",
"your_organization_updated_sucessfully": "Your organization updated successfully",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"team_no_event_types": "This team has no event types",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"include_calendar_event": "Include calendar event",
"recently_added":"Recently added",
"no_members_found": "No members found",
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
"availability_schedules":"Availability Schedules",
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
"edit_users_availability":"Edit user's availability: {{username}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+9 -11
View File
@@ -129,7 +129,7 @@
"team_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
"upgrade_banner_action": "Actualizar aquí",
"team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!",
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan Organization. Notamos que su Organization \"{{teamName}}\" necesita actualizarse.",
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
"hey_there": "Hola,",
@@ -306,7 +306,7 @@
"password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.",
"layout": "Diseño",
"bookerlayout_default_title": "Vista predeterminada",
"bookerlayout_description": "Puede seleccionar varios y quienes le reserven pueden cambiar de vista.",
"bookerlayout_description": "Puede seleccionar varias y quienes le reserven pueden cambiar de vista.",
"bookerlayout_user_settings_title": "Diseño de reserva",
"bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
"bookerlayout_month_view": "Mes",
@@ -315,7 +315,7 @@
"bookerlayout_error_min_one_enabled": "Al menos un diseño tiene que estar habilitado.",
"bookerlayout_error_default_not_enabled": "El diseño que seleccionó como vista predeterminada no es parte de los diseños habilitados.",
"bookerlayout_error_unknown_layout": "El diseño seleccionado no es un diseño válido.",
"bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia</2> o <6>sobrescribir solo para este evento</6>.",
"bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia</2> o <6>anular solo para este evento</6>.",
"unexpected_error_try_again": "Ocurrió un error inesperado. Inténtelo de nuevo.",
"sunday_time_error": "Hora inválida del domingo",
"monday_time_error": "Hora inválida del lunes",
@@ -551,7 +551,7 @@
"team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.",
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
"members": "Miembros",
"organization_members": "Miembros de la organización",
"organization_members": "Miembros de Organization",
"member": "Miembro",
"number_member_one": "{{count}} miembro",
"number_member_other": "{{count}} miembros",
@@ -699,7 +699,7 @@
"create_team_to_get_started": "Crea un equipo para empezar",
"teams": "Equipos",
"team": "Equipo",
"organization": "Organización",
"organization": "Organization",
"team_billing": "Facturación del equipo",
"team_billing_description": "Gestione la facturación para su equipo",
"upgrade_to_flexible_pro_title": "Hemos cambiado la facturación de los equipos",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Este evento se canceló",
"emailed_information_about_cancelled_event": "Le enviamos un correo electrónico a usted y a los demás asistentes para informarles.",
"this_input_will_shown_booking_this_event": "Esta entrada se mostrará al reservar este evento.",
"meeting_url_in_conformation_email": "La URL de la reunión está en el correo de confirmación",
"meeting_url_in_confirmation_email": "La URL de la reunión está en el correo de confirmación",
"url_start_with_https": "La URL debe empezar por http:// o https://",
"number_provided": "Se proporcionará el número de teléfono",
"before_event_trigger": "antes del inicio del evento",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "¿Está seguro de que desea eliminar la configuración de {{connectionType}}? Los miembros de su equipo que utilicen el inicio de sesión {{connectionType}} ya no podrán acceder a Cal.com.",
"organizer_timezone": "Zona horaria del organizador",
"email_user_cta": "Ver la invitación",
"email_no_user_invite_heading": "Lo invitaron a unirse a {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin necesidad de correos electrónicos.",
"email_user_invite_subheading_team": "{{invitedBy}} lo ha invitado a unirse a su equipo \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su equipo programar reuniones sin correos electrónicos de ida y vuelta.",
"email_no_user_invite_steps_intro": "Lo guiaremos a través de unos pocos pasos y disfrutará de una programación sin estrés con su {{entity}} en muy poco tiempo.",
"email_no_user_step_one": "Elija su nombre de usuario",
"email_no_user_step_two": "Conecte su cuenta de calendario",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Cree su primer webhook para este tipo de evento",
"install_app_on": "Instalar aplicación en",
"create_for": "Crear para",
"setup_organization": "Configurar una organización",
"organization_banner_description": "Cree entornos en los que sus equipos puedan crear aplicaciones, flujos de trabajo y tipos de eventos compartidos con programación por turnos y colectiva.",
"organization_banner_title": "Gestione organizaciones con varios equipos",
"set_up_your_organization": "Configure su organización",
@@ -1890,7 +1888,7 @@
"organization_name": "Nombre de la organización",
"organization_url": "URL de la organización",
"organization_verify_header": "Verifique el correo electrónico de su organización",
"organization_verify_email_body": "Utilice el código a continuación para verificar su dirección de correo electrónico para seguir configurando su organización.",
"organization_verify_email_body": "Utilice el siguiente código para verificar su dirección de correo electrónico y continuar con la configuración de su organización.",
"additional_url_parameters": "Parámetros adicionales de URL",
"about_your_organization": "Acerca de su organización",
"about_your_organization_description": "Las organizaciones son entornos compartidos donde puede crear varios equipos con miembros, tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.",
@@ -1918,7 +1916,7 @@
"org_no_teams_yet_description": "Si usted es un administrador, asegúrese de crear equipos para que se muestren aquí.",
"set_up": "Configurar",
"set_up_your_profile": "Configure su perfil",
"set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} cuándo interactúan con su enlace público.",
"set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} y cuándo interactúen con su enlace público.",
"my_profile": "Mi perfil",
"my_settings": "Mi configuración",
"crm": "CRM",
+44 -12
View File
@@ -266,6 +266,7 @@
"nearly_there_instructions": "Pour finir, une brève description de vous et une photo vous aideront vraiment à obtenir des réservations et à faire savoir aux gens avec qui ils prennent rendez-vous.",
"set_availability_instructions": "Définissez des plages de temps pendant lesquelles vous êtes disponible de manière récurrente. Vous pourrez en créer d'autres ultérieurement et les assigner à différents calendriers.",
"set_availability": "Définissez vos disponibilités",
"availability_settings": "Paramètres de disponibilité",
"continue_without_calendar": "Continuer sans calendrier",
"connect_your_calendar": "Connectez votre calendrier",
"connect_your_video_app": "Connectez vos applications vidéo",
@@ -290,9 +291,9 @@
"add_another_calendar": "Ajouter un autre calendrier",
"other": "Autre",
"email_sign_in_subject": "Votre lien de connexion pour {{appName}}",
"emailed_you_and_attendees": "Vous et tous les autres participants avez reçu ces informations par e-mail.",
"emailed_you_and_attendees_recurring": "Vous et tous les autres participants avez reçu par e-mail les informations pour le premier de ces événements récurrents.",
"emailed_you_and_any_other_attendees": "Vous et tous les autres participants avez reçu ces informations par e-mail.",
"emailed_you_and_attendees": "Nous avons envoyé un e-mail avec une invitation de calendrier comprenant les détails à tout le monde.",
"emailed_you_and_attendees_recurring": "Nous avons envoyé un e-mail avec une invitation de calendrier comprenant les détails à tout le monde pour le premier de ces événements récurrents.",
"emailed_you_and_any_other_attendees": "Nous avons envoyé cette information à tout le monde par e-mail.",
"needs_to_be_confirmed_or_rejected": "Votre réservation doit encore être confirmée ou refusée.",
"needs_to_be_confirmed_or_rejected_recurring": "Votre rendez-vous récurrent doit encore être confirmé ou refusé.",
"user_needs_to_confirm_or_reject_booking": "{{user}} doit encore confirmer ou refuser la réservation.",
@@ -590,6 +591,8 @@
"invite_new_member": "Inviter un nouveau membre d'équipe",
"invite_new_member_description": "Remarque : cela <1>coûtera une place supplémentaire (15 $/mois)</1> sur votre abonnement.",
"invite_new_team_member": "Invitez quelqu'un à rejoindre votre équipe.",
"upload_csv_file": "Télécharger un fichier .csv",
"invite_via_email": "Inviter par e-mail",
"change_member_role": "Changer le rôle du membre de l'équipe",
"disable_cal_branding": "Désactiver les références à la marque {{appName}}",
"disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}} de vos pages publiques.",
@@ -678,8 +681,8 @@
"new_event_type_btn": "Nouveau type d'événement",
"new_event_type_heading": "Créez votre premier type d'événement",
"new_event_type_description": "Les types d'événements vous permettent de partager des liens qui affichent vos disponibilités sur votre calendrier et permettent aux personnes de réserver des créneaux.",
"event_type_created_successfully": "Type d'événement {{eventTypeTitle}} créé avec succès",
"event_type_updated_successfully": "Type d'événement {{eventTypeTitle}} mis à jour avec succès",
"event_type_created_successfully": "Type d'événement créé avec succès",
"event_type_updated_successfully": "Type d'événement mis à jour avec succès",
"event_type_deleted_successfully": "Type d'événement supprimé avec succès",
"hours": "heures",
"people": "Personnes",
@@ -972,6 +975,8 @@
"offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.",
"seats_available_one": "Place disponible",
"seats_available_other": "Places disponibles",
"seats_nearly_full": "Places presque toutes occupées",
"seats_half_full": "Les places partent vite",
"number_of_seats": "Nombre de places par réservation",
"enter_number_of_seats": "Saisir le nombre de sièges",
"you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.",
@@ -1052,12 +1057,15 @@
"how_you_want_add_cal_site": "Comment souhaitez-vous ajouter {{appName}} à votre site ?",
"choose_ways_put_cal_site": "Choisissez l'une des méthodes suivantes pour mettre {{appName}} sur votre site.",
"setting_up_zapier": "Configuration de votre intégration Zapier",
"setting_up_make": "Configuration de votre intégration Make",
"generate_api_key": "Générer une clé API",
"generate_api_key_description": "Générer une clé API à utiliser avec {{appName}} sur",
"your_unique_api_key": "Votre clé API unique",
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application Trigger. Choisissez également un événement Trigger.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre Trigger.</3><4>Vous êtes prêt !</4>",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"make_setup_instructions": "<0>Accédez au <1><0>lien d'invitation Make</0></1> et installez l'application Cal.com.</0><1>Connectez-vous à votre compte Make et créez un nouveau scénario.</1><2>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</2><3>Choisissez votre compte, puis saisissez votre clé API unique.</3><4>Testez votre déclencheur.</4><5>Vous êtes prêt !</5>",
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
"install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
"connect_apple_server": "Se connecter au serveur d'Apple",
"calendar_url": "Lien du calendrier",
"apple_server_generate_password": "Générez un mot de passe pour application à utiliser avec {{appName}} sur",
@@ -1095,9 +1103,9 @@
"reschedule_optional": "Raison de la replanification (facultatif)",
"reschedule_placeholder": "Faites savoir aux autres pourquoi vous devez replanifier le rendez-vous.",
"event_cancelled": "Cet événement est annulé",
"emailed_information_about_cancelled_event": "Nous vous avons envoyé un e-mail à vous et aux autres participants pour les en informer.",
"emailed_information_about_cancelled_event": "Nous avons envoyé un e-mail à tout le monde pour les en informer.",
"this_input_will_shown_booking_this_event": "Ce champ sera affiché lors de la réservation de cet événement.",
"meeting_url_in_conformation_email": "Le lien du rendez-vous est dans l'e-mail de confirmation",
"meeting_url_in_confirmation_email": "Le lien du rendez-vous est dans l'e-mail de confirmation",
"url_start_with_https": "Le lien doit commencer par http:// ou https://",
"number_provided": "Un numéro de téléphone sera fourni",
"before_event_trigger": "avant le début de l'événement",
@@ -1107,6 +1115,7 @@
"email_attendee_action": "envoyer un e-mail aux participants",
"sms_attendee_action": "Envoyer un SMS au participant",
"sms_number_action": "envoyer un SMS à un numéro spécifique",
"send_reminder_sms": "Envoyez facilement des rappels de rendez-vous par SMS à vos participants",
"whatsapp_number_action": "envoyer un message WhatsApp à un numéro spécifique",
"whatsapp_attendee_action": "envoyer un message WhatsApp au participant",
"workflows": "Workflows",
@@ -1114,7 +1123,7 @@
"add_new_workflow": "Ajouter un nouveau workflow",
"reschedule_event_trigger": "lorsque l'événement est replanifié",
"trigger": "Déclencheur",
"triggers": "Déclencher",
"triggers": "Déclencheurs",
"action": "Action",
"workflows_to_automate_notifications": "Créez des workflows pour automatiser les notifications et les rappels.",
"workflow_name": "Nom du workflow",
@@ -1449,6 +1458,7 @@
"add_limit": "Ajouter une limite",
"team_name_required": "Nom d'équipe requis",
"show_attendees": "Partagez les informations des participants entre les invités",
"show_available_seats_count": "Afficher le nombre de places disponibles",
"how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?",
"format": "Format",
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
@@ -1682,9 +1692,11 @@
"delete_sso_configuration_confirmation_description": "Voulez-vous vraiment supprimer la configuration {{connectionType}} ? Les membres de votre équipe utilisant la connexion {{connectionType}} ne pourront plus accéder à Cal.com.",
"organizer_timezone": "Fuseau horaire de l'organisateur",
"email_user_cta": "Voir l'invitation",
"email_no_user_invite_heading": "Vous avez été invité à rejoindre une {{entity}} {{appName}}",
"email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}",
"email_no_user_invite_heading_org": "Vous avez été invité à rejoindre une organisation {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son {{entity}} « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre {{entity}} d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_org": "{{invitedBy}} vous a invité à rejoindre son organisation « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre organisation d'organiser des rendez-vous sans échanges d'e-mails.",
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} 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",
@@ -1907,7 +1919,6 @@
"install_app_on": "Installer l’application sur",
"create_for": "Créer pour",
"currency": "Devise",
"setup_organization": "Configurer une organisation",
"organization_banner_description": "Créez un environnement où vos équipes peuvent créer des applications partagées, des workflows et des types d'événements avec une planification round-robin et collective.",
"organization_banner_title": "Gérer les organisations avec plusieurs équipes",
"set_up_your_organization": "Configurer votre organisation",
@@ -1988,6 +1999,8 @@
"remove_users_from_org_confirm": "Voulez-vous vraiment supprimer {{userCount}} utilisateurs de cette organisation ?",
"user_has_no_schedules": "Cet utilisateur n'a pas encore configuré de planning",
"user_isnt_in_any_teams": "Cet utilisateur n'est dans aucune équipe",
"requires_booker_email_verification": "Nécessite la vérification par e-mail du participant",
"description_requires_booker_email_verification": "Pour assurer la vérification par e-mail du participant avant la planification des événements.",
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
"kyc_verification_information": "Pour garantir la sécurité, vous devez vérifier votre {{teamOrAccount}} avant d'envoyer des messages aux participants. Veuillez nous contacter à <a>{{supportEmail}}</a> et fournir les informations suivantes :",
"kyc_verification_documents": "<ul><li>Votre {{teamOrUser}}</li><li>Pour les entreprises : Joignez votre document de vérification d'entreprise</li><li>Pour les particuliers : Joignez une pièce d'identité</li></ul>",
@@ -2001,16 +2014,28 @@
"no_other_teams_found_description": "Il n'y a pas d'autres équipes dans cette organisation.",
"attendee_first_name_variable": "Prénom du participant",
"attendee_last_name_variable": "Nom du participant",
"attendee_first_name_info": "Le prénom du participant",
"attendee_last_name_info": "Le nom de famille du participant",
"me": "Moi",
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
"member_removed": "Membre supprimé",
"my_availability": "Mes disponibilités",
"team_availability": "Disponibilités de l'équipe",
"backup_code": "Code de récupération",
"backup_codes": "Codes de récupération",
"backup_code_instructions": "Chaque code de récupération peut être utilisé une seule fois pour accorder l'accès sans votre authentificateur.",
"backup_codes_copied": "Codes de récupération copiés !",
"incorrect_backup_code": "Le code de récupération est incorrect.",
"lost_access": "Accès perdu",
"missing_backup_codes": "Aucun code de récupération trouvé. Merci de les générer dans vos paramètres.",
"admin_org_notification_email_subject": "Nouvelle organisation créée : action en attente",
"hi_admin": "Bonjour administrateur",
"admin_org_notification_email_title": "Une organisation nécessite une configuration DNS",
"admin_org_notification_email_body_part1": "Une organisation avec le slug « {{orgSlug}} » a été créée.<br /><br />Assurez-vous de configurer votre registre DNS pour pointer le sous-domaine correspondant à la nouvelle organisation où l'application principale est en cours d'exécution. Autrement, l'organisation ne fonctionnera pas.<br /><br />Voici les options de base pour configurer un sous-domaine pour qu'il pointe vers son application afin qu'il charge la page de profil de l'organisation.<br /><br />Vous pouvez le faire soit avec l'enregistrement A :",
"admin_org_notification_email_body_part2": "Ou l'enregistrement CNAME :",
"admin_org_notification_email_body_part3": "Une fois le sous-domaine configuré, veuillez marquer la configuration DNS comme terminée dans les paramètres d'administration des organisations.",
"admin_org_notification_email_cta": "Accéder aux paramètres d'administration des organisations",
"org_has_been_processed": "L'organisation a été traitée",
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
"unverified": "Non vérifié",
@@ -2018,5 +2043,12 @@
"mark_dns_configured": "Marquer comme DNS configuré",
"value": "Valeur",
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
"include_calendar_event": "Inclure l'événement du calendrier",
"recently_added": "Ajouté récemment",
"no_members_found": "Aucun membre trouvé",
"event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.",
"availability_schedules": "Horaires de disponibilité",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1075,7 +1075,7 @@
"event_cancelled": "האירוע בוטל",
"emailed_information_about_cancelled_event": "שלחנו לך ולשאר המשתתפים הודעת דוא״ל כדי ליידע אותם על כך.",
"this_input_will_shown_booking_this_event": "הקלט יוצג בעת הזמנת אירוע זה",
"meeting_url_in_conformation_email": "כתובת ה-URL של הפגישה נמצאת בהודעת הדוא״ל שנשלחה לאישור הפגישה",
"meeting_url_in_confirmation_email": "כתובת ה-URL של הפגישה נמצאת בהודעת הדוא״ל שנשלחה לאישור הפגישה",
"url_start_with_https": "כתובת ה-URL צריכה להתחיל עם הקידומת http://‎ או https://‎",
"number_provided": "מספר הטלפון יסופק",
"before_event_trigger": "לפני שהאירוע מתחיל",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "אתה בטוח שאתה רוצה למחוק את הגדרות {{connectionType}}? חברי הצוות שלך שמשתמשים ב- {{connectionType}} להזדהות לא יוכלו להשתמש בו להכנס ל- Cal.com.",
"organizer_timezone": "מארגן אזורי זמן",
"email_user_cta": "צפה בהזמנה",
"email_no_user_invite_heading": "הוזמנת להצטרף ל{{entity}} של {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
"email_user_invite_subheading": "{{invitedBy}} הזמין/ה אותך להצטרף ל-{{entity}} שלו/ה בשם ״{{teamName}}״ באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ול-{{entity}} שלך לתזמן פגישות בלי הצורך לנהל התכתבויות ארוכות בדוא״ל.",
"email_user_invite_subheading_team": "{{invitedBy}} הזמין/ה אותך להצטרף לצוות שלו/ה בשם '{{teamName}}' באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ולצוות שלך לתזמן פגישות בלי כל הפינג פונג במיילים.",
"email_no_user_invite_steps_intro": "נדריך אותך במספר קטן של צעדים ותוכל/י להתחיל ליהנות מקביעת מועדים עם ה-{{entity}} שלך במהירות ובלי בעיות.",
"email_no_user_step_one": "בחר שם משתמש",
"email_no_user_step_two": "קשר את לוח השנה שלך",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
"install_app_on": "התקנת האפליקציה ב-",
"create_for": "צור/צרי עבור",
"setup_organization": "הגדרת Organization",
"organization_banner_description": "צור/צרי סביבות שבהן הצוותים שלך יוכלו ליצור אפליקציות, תהליכי עבודה וסוגי אירועים משותפים, עם תכונות כמו סבב וקביעת מועדים שיתופית.",
"organization_banner_title": "ניהול ארגונים עם צוותים מרובים",
"set_up_your_organization": "הגדרת הארגון שלך",

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