Compare commits

...
59 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
185 changed files with 2375 additions and 836 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
View File
@@ -7,7 +7,6 @@ public
*.lock
*.log
*.test.ts
.gitignore
.npmignore
+1 -1
View File
@@ -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'
+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,
+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 -2
View File
@@ -1,2 +1 @@
public/embed
*.test.ts
public/embed
@@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
type={item.integration.type}
isChecked={cal.isSelected}
destination={cal.externalId === props.destinationCalendarId}
credentialId={cal.credentialId}
/>
))}
</ul>
@@ -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>
)}
/>
@@ -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"}
+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
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.9",
"version": "3.3.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+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: {},
+1 -8
View File
@@ -52,8 +52,6 @@ export default function Login({
totpEmail,
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const searchParams = useSearchParams();
const isTeamInvite = searchParams.get("teamInvite");
const { t } = useLocale();
const router = useRouter();
const formSchema = z
@@ -97,9 +95,7 @@ export default function Login({
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<a
href={callbackUrl !== "" ? `${WEBSITE_URL}/signup?callbackUrl=${callbackUrl}` : `${WEBSITE_URL}/signup`}
className="text-brand-500 font-medium">
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
{t("dont_have_an_account")}
</a>
);
@@ -188,9 +184,6 @@ export default function Login({
? LoginFooter
: null
}>
{isTeamInvite && (
<Alert severity="info" message={t("signin_or_signup_to_accept_invite")} className="mb-4 mt-4" />
)}
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
<div>
+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) {
+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<
@@ -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;
@@ -360,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
children,
@@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
durationLimits,
recurringEvent,
@@ -516,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
seatsShowAvailabilityCount,
metadata,
customInputs,
});
+4 -2
View File
@@ -715,14 +715,16 @@ 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}${teamId ? "/team" : ""}/${profile.slug}/avatar.png` ||
undefined
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
size="md"
className="mt-1 inline-flex justify-center"
+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"}
+8 -22
View File
@@ -13,7 +13,6 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@@ -35,23 +34,6 @@ type FormValues = z.infer<typeof signupSchema>;
type SignupProps = inferSSRProps<typeof getServerSideProps>;
const getSafeCallbackUrl = (url: string | null) => {
if (!url) return null;
let callbackUrl = url;
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
return safeCallbackUrl;
};
export default function Signup({ prepopulateFormValues, token, orgSlug }: SignupProps) {
const searchParams = useSearchParams();
const telemetry = useTelemetry();
@@ -73,7 +55,6 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
throw new Error(err.message);
}
};
const callbackUrl = getSafeCallbackUrl(searchParams.get("callbackUrl"));
const signUp: SubmitHandler<FormValues> = async (data) => {
await fetch("/api/auth/signup", {
@@ -91,10 +72,13 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
.then(async () => {
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
await signIn<"credentials">("credentials", {
...data,
callbackUrl: `${callbackUrl ? callbackUrl : `${WEBAPP_URL}/${verifyOrGettingStarted}`}?from=signup`,
callbackUrl: `${
searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
}?from=signup`,
});
})
.catch((err) => {
@@ -173,7 +157,9 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
className="w-full justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: callbackUrl ? callbackUrl : `${WEBAPP_URL}/getting-started`,
callbackUrl: searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}
+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">
-17
View File
@@ -1,11 +1,9 @@
import type { GetServerSidePropsContext } from "next";
import { getLayout } from "@calcom/features/MainLayout";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import { ShellMain } from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@@ -43,21 +41,6 @@ function Teams() {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({ req: context.req, res: context.res });
const token = context.query?.token;
const resolvedUrl = context.resolvedUrl;
const callbackUrl = token ? getSafeRedirectUrl(`${WEBAPP_URL}${resolvedUrl}`) : null;
if (!session) {
return {
redirect: {
destination: callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}&teamInvite=true` : "/auth/login",
permanent: false,
},
props: {},
};
}
return { props: { trpcState: ssr.dehydrate() } };
};
+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", () => {
+3 -2
View File
@@ -4,6 +4,7 @@ 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";
@@ -61,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,
},
@@ -138,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);
@@ -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,",
@@ -1681,7 +1681,7 @@
"email_user_cta": "Einladung anzeigen",
"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_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 Ihrem {{entity}} genießen.",
"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",
+12 -5
View File
@@ -45,7 +45,6 @@
"invite_team_individual_segment": "Invite individual",
"invite_team_bulk_segment": "Bulk import",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "You need to Sign in or Sign up to see team invitation.",
"your_event_has_been_scheduled": "Your event has been scheduled",
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
@@ -292,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.",
@@ -592,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.",
@@ -974,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.",
@@ -1100,7 +1103,7 @@
"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_confirmation_email": "Meeting url is in the confirmation email",
"url_start_with_https": "URL needs to start with http:// or https://",
@@ -1455,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",
@@ -2046,5 +2050,8 @@
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1888,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.",
@@ -45,7 +45,6 @@
"invite_team_individual_segment": "Inviter une personne",
"invite_team_bulk_segment": "Importation multiple",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "Vous devez vous connecter ou vous inscrire pour voir l'invitation d'équipe.",
"your_event_has_been_scheduled": "Votre événement a été planifié",
"your_event_has_been_scheduled_recurring": "Votre événement récurrent a été planifié",
"accept_our_license": "Acceptez notre licence en changeant la variable .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> en « {{agree}} ».",
@@ -292,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.",
@@ -592,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.",
@@ -974,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é.",
@@ -1100,7 +1103,7 @@
"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_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://",
@@ -1455,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.",
@@ -210,6 +210,8 @@
"done": "Učinjeno",
"all_done": "Završeno!",
"all": "Sve",
"available_apps": "Dostupne Aplikacije",
"available_apps_lower_case": "Dostupne aplikacije",
"check_email_reset_password": "Provjerite svoju e-poštu. Poslali smo vam link za resetiranje lozinke.",
"finish": "Završi",
"few_sentences_about_yourself": "Nekoliko rečenica o sebi. Ovo će se pojaviti na vašoj osobnoj stranici.",
@@ -331,5 +333,6 @@
"dark": "Tamna",
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
"full_name": "Puno ime",
"insights_all_org_filter": "Sve"
}
+14 -14
View File
@@ -308,7 +308,7 @@
"layout": "Raspored",
"bookerlayout_default_title": "Podrazumevani prikaz",
"bookerlayout_description": "Možete da izaberete više njih, a vaši učesnici mogu da menjaju prikaze.",
"bookerlayout_user_settings_title": "Raspored zakazivanja",
"bookerlayout_user_settings_title": "Režim prikaza rezervacija",
"bookerlayout_user_settings_description": "Možete da izaberete više njih, a učesnici mogu da menjaju prikaz. Ovo se može zameniti za svaki događaj.",
"bookerlayout_month_view": "Mesec",
"bookerlayout_week_view": "Nedeljno",
@@ -404,7 +404,7 @@
"recording_ready": "Link za preuzimanje snimka je spreman",
"booking_created": "Rezervacija Napravljena",
"booking_rejected": "Rezevacija je odbijena",
"booking_requested": "Rezervacija je zahtevana",
"booking_requested": "Zahtev za rezervaciju je poslat",
"meeting_ended": "Sastanak se završio",
"form_submitted": "Formular poslat",
"event_triggers": "Okidači Dogadjaja",
@@ -552,11 +552,11 @@
"team_description": "Par rečenica o vašem timu. Ovo će se pojaviti na stranici URL adrese vašeg tima.",
"org_description": "Nekoliko rečenica o vašoj organizaciji. Ovo će se pojaviti na url stranici vaše organizacije.",
"members": "Članovi",
"organization_members": "Članovi organizacije",
"organization_members": "Korisnici uključeni u tarifni plan Organization",
"member": "Član",
"number_member_one": "{{count}} član",
"number_member_other": "{{count}} članova",
"number_selected": "{{count}} izabrano",
"number_selected": "Izabrano: {{count}}",
"owner": "Vlasnik",
"admin": "Admin",
"administrator_user": "Administrator",
@@ -1687,7 +1687,7 @@
"attendee_no_longer_attending": "Polaznik više ne pohađa vaš događaj",
"attendee_no_longer_attending_subtitle": "Korisnik {{name}} je otkazao. To znači da se otvorilo mesto za ovaj vremenski period",
"create_event_on": "Kreirajte događaj na",
"create_routing_form_on": "Kreiraj obrazac za usmeravanje",
"create_routing_form_on": "Kreiranje obrasca za usmeravanje za",
"default_app_link_title": "Podesite podrazumevani link aplikacije",
"default_app_link_description": "Podešavanje podrazumevanog linka aplikacije omogućava novokreiranim tipovima događaja da koriste link aplikacije koji ste postavili.",
"organizer_default_conferencing_app": "Podrazumevana aplikacija organizatora",
@@ -1876,23 +1876,23 @@
"connect_google_workspace": "Poveži Google Workspace",
"google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju",
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
"install_app_on": "Instaliraj aplikaciju",
"install_app_on": "Instaliraj aplikaciju za",
"create_for": "Napravi za",
"organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
"organization_banner_description": "Kreirajte okruženja gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
"organization_banner_title": "Upravljajte organizacijama sa više timova",
"set_up_your_organization": "Postavite svoju organizaciju",
"set_up_your_organization": "Konfigurisanje profila organizacije",
"organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.",
"must_enter_organization_name": "Morate da unesete naziv organizacije",
"must_enter_organization_admin_email": "Morate da unesete adresu e-pošte vaše organizacije",
"admin_email": "Imejl adresa vaše organizacije",
"admin_email": "Vaša adresa e-pošte u organizaciji",
"admin_username": "Korisničko ime administratora",
"organization_name": "Naziv organizacije",
"organization_url": "URL organizacije",
"organization_verify_header": "Potvrdite imejl vaše organizacije",
"organization_verify_header": "Potvrdite svoju adresu e-pošte u organizaciji",
"organization_verify_email_body": "Koristite kôd u nastavku da potvrdite svoju adresu e-pošte kako biste nastavili sa podešavanjem svoje organizacije.",
"additional_url_parameters": "Dodatni URL parametri",
"about_your_organization": "O vašoj organizaciji",
"about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrste događaja, aplikacije, radne tokove i drugo.",
"about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrstama događaja, aplikacijama, radnim tokovima i mnogim drugim stvarima.",
"create_your_teams": "Kreirajte svoje timove",
"create_your_teams_description": "Počnite planiranje zajedno dodavanjem članova tima u svoju organizaciju",
"invite_organization_admins": "Pozovite administratore vaše organizacije",
@@ -1917,7 +1917,7 @@
"org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.",
"set_up": "Podesi",
"set_up_your_profile": "Podesite svoj profil",
"set_up_your_profile_description": "Neka ljudi u organizaciji {{orgName}} znaju ko ste i kako da se povežu sa vama putem javnog linka.",
"set_up_your_profile_description": "Unesite informacije o svojoj ulozi u organizaciji {{orgName}}. Te informacije će videti osobe koje kliknu na vaš javni link.",
"my_profile": "Moj profil",
"my_settings": "Moja podešavanja",
"crm": "CRM",
@@ -1925,7 +1925,7 @@
"sender_id_info": "Ime ili broj koji će biti prikazani kao pošiljalac SMS-a (neke zemlje ne dozvoljavaju alfanumeričke ID-ove pošiljaoca)",
"org_admins_can_create_new_teams": "Samo administrator vaše ogranizacije može da kreira nove timove",
"google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.",
"resolve": "Reši",
"resolve": "Kako rešiti taj problem",
"no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.",
"org_name": "Naziv organizacije",
"org_url": "URL organizacije",
@@ -1933,7 +1933,7 @@
"404_the_org": "Organizacija",
"404_the_team": "Tim",
"404_claim_entity_org": "Zatražite poddomen za svoju organizaciju",
"404_claim_entity_team": "Zahtevajte ovaj tim i počnite da uređujete kolektivni raspored",
"404_claim_entity_team": "Postanite deo ovog tima i počnite da uređujete kolektivni raspored",
"insights_all_org_filter": "Sve aplikacije",
"insights_team_filter": "Tim: {{teamName}}",
"insights_user_filter": "Korisnik: {{userName}}",
@@ -17,9 +17,15 @@
"verify_email_banner_body": "Verifiera din e-postadress för att garantera bästa e-post- och kalenderleverans",
"verify_email_email_header": "Verifiera din e-postadress",
"verify_email_email_button": "Verifiera e-post",
"copy_somewhere_safe": "Spara denna API-nyckel någonstans säkert. Du kommer inte att kunna visa den igen.",
"verify_email_email_body": "Verifiera din e-postadress genom att klicka på knappen nedan.",
"verify_email_by_code_email_body": "Verifiera din e-postadress genom att använda nedanstående kod.",
"verify_email_email_link_text": "Här är länken om du inte gillar att klicka på knappar:",
"email_verification_code": "Ange verifieringskod",
"email_verification_code_placeholder": "Ange verifieringskoden som skickats till din e-post",
"incorrect_email_verification_code": "Vertifieringskoden är felaktig.",
"email_sent": "E-postmeddelandet har skickats",
"email_not_sent": "Ett fel uppstod när e-post skulle skickas",
"event_declined_subject": "Avvisades: {{title}} kl. {{date}}",
"event_cancelled_subject": "Avbokad: {{title}} {{date}}",
"event_request_declined": "Din bokningsförfrågan har avbjöts",
@@ -78,6 +84,7 @@
"event_awaiting_approval_recurring": "En återkommande händelse väntar på ditt godkännande",
"someone_requested_an_event": "Någon har begärt att schemalägga en händelse i din kalender.",
"someone_requested_password_reset": "Någon har begärt en länk för att ändra ditt lösenord.",
"password_reset_email_sent": "Om denna e-postadress finns i vårt system bör du få ett återställningsmeddelande.",
"password_reset_instructions": "Om du inte begärde detta kan du ignorera detta e-postmeddelande och ditt lösenord kommer inte att ändras.",
"event_awaiting_approval_subject": "Inväntar godkännande: {{title}} den {{date}}",
"event_still_awaiting_approval": "En bokning väntar fortfarande på ditt godkännande",
@@ -217,6 +224,10 @@
"already_have_an_account": "Har du redan ett konto?",
"create_account": "Skapa konto",
"confirm_password": "Bekräfta lösenord",
"confirm_auth_change": "Detta kommer att ändra hur du loggar in",
"confirm_auth_email_change": "Att ändra e-postadressen kommer att koppla bort din nuvarande autentiseringsmetod för att logga in på Cal.com. Vi kommer att be dig verifiera din nya e-postadress. Framå kommer du att loggas ut och använda din nya e-postadress för att logga in istället för din nuvarande autentiseringsmetod efter att du ställt in ditt lösenord genom att följa instruktionerna som kommer att skickas till din e-post.",
"reset_your_password": "Konfigurera ditt nya lösenord med instruktionerna som skickas till din e-postadress.",
"email_change": "Logga in igen med din nya e-postadress och lösenord.",
"create_your_account": "Skapa ditt konto",
"sign_up": "Registrera dig",
"youve_been_logged_out": "Du har loggats ut",
@@ -243,6 +254,10 @@
"all": "Alla",
"yours": "Ditt konto",
"available_apps": "Tillgängliga appar",
"available_apps_lower_case": "Tillgängliga appar",
"available_apps_desc": "Du har inga appar installerade. Visa populära appar nedan och utforska mer i vår <1>App Store</1>",
"fixed_host_helper": "Lägg till alla som behöver delta i evenemanget. <1>Läs mer</1>",
"round_robin_helper": "Personer i gruppen turas om och endast en person kommer att dyka upp för evenemanget.",
"check_email_reset_password": "Kolla din e-post. Vi har skickat en länk till dig för att återställa ditt lösenord.",
"finish": "Slutför",
"organization_general_description": "Hantera inställningar för ditt teams språk och tidszon",
@@ -251,6 +266,7 @@
"nearly_there_instructions": "Till sist så hjälper en kort beskrivning om dig själv och ett foto dig verkligen att få bokningar och andra kan veta vem de bokar med.",
"set_availability_instructions": "Definiera tidsintervall när du är tillgänglig på en återkommande basis. Du kan skapa fler av dessa senare och tilldela dem till olika kalendrar.",
"set_availability": "Ställ in din tillgänglighet",
"availability_settings": "Tillgänglighetsinställningar",
"continue_without_calendar": "Fortsätt utan kalender",
"connect_your_calendar": "Anslut din kalender",
"connect_your_video_app": "Anslut dina videoappar",
@@ -392,6 +408,8 @@
"allow_dynamic_booking_tooltip": "Gruppbokningslänkar som kan skapas dynamiskt genom att lägga till flera användarnamn med ett '+'. Exempel: '{{appName}}/bailey+peer'",
"allow_dynamic_booking": "Tillåt deltagare att boka dig via dynamiska gruppbokningar",
"dynamic_booking": "Dynamiska grupplänkar",
"allow_seo_indexing": "Tillåt sökmotorer att komma åt ditt publika innehåll",
"seo_indexing": "Tillåt SEO-indexering",
"email": "E-post",
"email_placeholder": "jdoe@exempel.se",
"full_name": "Fullständigt namn",
@@ -406,6 +424,7 @@
"booking_requested": "Bokning begärd",
"meeting_ended": "Mötet avslutat",
"form_submitted": "Formulär skickat",
"booking_paid": "Bokning betald",
"event_triggers": "Händelseutlösare",
"subscriber_url": "Prenumerantens URL",
"create_new_webhook": "Skapa en ny webhook",
@@ -506,6 +525,7 @@
"your_name": "Ditt namn",
"your_full_name": "Ditt fullständiga namn",
"no_name": "Inget namn",
"enter_number_between_range": "Ange ett tal mellan 1 och {{maxOccurences}}",
"email_address": "E-postadress",
"enter_valid_email": "Ange en giltig e-postadress",
"location": "Plats",
@@ -543,6 +563,7 @@
"leave": "Lämna",
"profile": "Profil",
"my_team_url": "Min team-URL",
"my_teams": "Mina team",
"team_name": "Teamnamn",
"your_team_name": "Ditt team-namn",
"team_updated_successfully": "Teamet har uppdaterats",
@@ -570,6 +591,8 @@
"invite_new_member": "Bjud in en ny teammedlem",
"invite_new_member_description": "Obs! Detta kommer att <1>kosta en extra plats (150 kr/mån)</1> i din prenumeration.",
"invite_new_team_member": "Bjud in någon till ditt team.",
"upload_csv_file": "Ladda upp en .csv-fil",
"invite_via_email": "Bjud in via e-post",
"change_member_role": "Ändra teammedlemmens roll",
"disable_cal_branding": "Inaktivera {{appName}}-varumärke",
"disable_cal_branding_description": "Dölj all {{appName}} reklam från dina offentliga sidor.",
@@ -842,6 +865,7 @@
"team_view_user_availability_disabled": "Användaren måste acceptera inbjudan för att visa tillgängligheten",
"set_as_away": "Ställ in dig själv som inte tillgänglig",
"set_as_free": "Ställ av din status som inte tillgänglig",
"toggle_away_error": "Fel vid uppdatering av frånvarostatus",
"user_away": "Den här användaren är för inte tillgänglig.",
"user_away_description": "Personen du försöker boka har satt sig som inte tillgänglig och accepterar därför inte nya bokningar.",
"meet_people_with_the_same_tokens": "Träffa personer med samma tokens",
@@ -851,6 +875,7 @@
"account_managed_by_identity_provider_description": "Besök dina {{provider}} -kontoinställningar för att ändra din e-post, lösenord, aktivera tvåfaktorsautentisering mm.",
"signin_with_google": "Logga in med Google",
"signin_with_saml": "Logga in med SAML",
"signin_with_saml_oidc": "Logga in med SAML/OIDC",
"you_will_need_to_generate": "Du måste generera en åtkomsttoken från ditt gamla schemaläggningsverktyg.",
"import": "Importera",
"import_from": "Importera från",
@@ -950,6 +975,8 @@
"offer_seats_description": "Erbjud platser till bokningar. Detta inaktiverar gäster och bokningar.",
"seats_available_one": "Tillgänglig plats",
"seats_available_other": "Tillgängliga platser",
"seats_nearly_full": "Sittplatser nästan fulla",
"seats_half_full": "Sittplatser fylls snabbt",
"number_of_seats": "Antal platser per bokning",
"enter_number_of_seats": "Ange antal platser",
"you_can_manage_your_schedules": "Du kan hantera dina scheman på sidan Tillgänglighet.",
@@ -1030,12 +1057,15 @@
"how_you_want_add_cal_site": "Hur vill du lägga till {{appName}} till din webbplats?",
"choose_ways_put_cal_site": "Välj ett av följande sätt att lägga {{appName}} på din webbplats.",
"setting_up_zapier": "Ställa in din Zapier-integration",
"setting_up_make": "Konfigurera din Make-integration",
"generate_api_key": "Generera API-nyckel",
"generate_api_key_description": "Skapa en API-nyckel att använda med {{appName}} på",
"your_unique_api_key": "Din unika API-nyckel",
"copy_safe_api_key": "Kopiera denna API-nyckel och spara den någonstans säkert. Om du förlorar denna nyckel måste du skapa en ny.",
"zapier_setup_instructions": "<0>Logga in på ditt Zapier-konto och skapa en ny Zap.</0><1>Välj {{appName}} som din Trigger-app. Välj också en Trigger-händelse. /1><2>Välj ditt konto och ange sedan din unika API-nyckel.</2><3>Testa din Trigger.</3><4>Du är klar!</4>",
"make_setup_instructions": "<0>Gå till <1><0>Skapa inbjudningslänk</0></1> och installera Cal.com-appen.</0><1>Logga in på ditt Make-konto och skapa ett nytt Scenario.</1><2>Välj Cal.com som din Trigger-app. Välj också en triggerhändelse.</2><3>Välj ditt konto och ange sedan din unika API-nyckel.</3><4>Testa din trigger.</4><5>Du är redo!</5>",
"install_zapier_app": "Installera först Zapier-appen i appbutiken.",
"install_make_app": "Installera först Make-appen i appbutiken.",
"connect_apple_server": "Anslut till Apple Server",
"calendar_url": "Kalender-URL",
"apple_server_generate_password": "Skapa ett appspecifikt lösenord som du kan använda med {{appName}} på",
@@ -1085,6 +1115,7 @@
"email_attendee_action": "skicka e-post till deltagare",
"sms_attendee_action": "Skicka SMS till deltagare",
"sms_number_action": "skicka SMS till ett specifikt nummer",
"send_reminder_sms": "Skicka enkelt mötespåminnelser via SMS till dina deltagare",
"whatsapp_number_action": "skicka Whatsapp till ett specifikt nummer",
"whatsapp_attendee_action": "skicka Whatsapp till deltagare",
"workflows": "Arbetsflöden",
@@ -1244,6 +1275,7 @@
"error_updating_settings": "Fel vid uppdatering av inställningar",
"personal_cal_url": "Min personliga {{appName}}-URL",
"bio_hint": "Några meningar om dig själv. Detta kommer att visas på din personliga webbsida.",
"user_has_no_bio": "Den här användaren har inte lagt till en biografi ännu.",
"delete_account_modal_title": "Radera konto",
"confirm_delete_account_modal": "Är du säker på att du vill radera ditt {{appName}}-konto?",
"delete_my_account": "Radera mitt konto",
@@ -1426,6 +1458,7 @@
"add_limit": "Lägg till gräns",
"team_name_required": "Teamnamn krävs",
"show_attendees": "Dela deltagarinformation mellan gäster",
"show_available_seats_count": "Visa antalet lediga platser",
"how_booking_questions_as_variables": "Hur används bokningsfrågor som variabler?",
"format": "Formatera",
"uppercase_for_letters": "Använd versaler för alla bokstäver",
@@ -1659,8 +1692,11 @@
"delete_sso_configuration_confirmation_description": "Vill du verkligen ta bort {{connectionType}}-konfigurationen? Dina teammedlemmar som använder {{connectionType}}-inloggning kan inte längre komma åt Cal.com.",
"organizer_timezone": "Organisatörens tidszon",
"email_user_cta": "Visa inbjudan",
"email_no_user_invite_heading_team": "Du har blivit inbjuden att gå med i ett {{appName}}-team",
"email_no_user_invite_heading_org": "Du har blivit inbjuden att gå med i en {{appName}}-organisation",
"email_no_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i sitt team på {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading_team": "{{invitedBy}} har bjudit in dig att gå med i sitt team {{teamName}} den {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading_org": "{{invitedBy}} har bjudit in dig att gå med i deras organisation \"{{teamName}}\" på {{appName}}. {{appName}} är schemaläggaren som gör det möjligt för dig och din organisation att schemalägga möten utan e-posttennis.",
"email_no_user_invite_steps_intro": "Vi guidar dig genom några korta steg och du kommer att kunna schemalägga med din/ditt {{entity}} på nolltid utan stress.",
"email_no_user_step_one": "Välj ditt användarnamn",
"email_no_user_step_two": "Anslut ditt kalenderkonto",
@@ -1831,6 +1867,8 @@
"insights_no_data_found_for_filter": "Inga data hittades för valt filter eller valda datum.",
"acknowledge_booking_no_show_fee": "Jag bekräftar att en avgift på {{amount, currency}} för utebliven närvaro debiteras mitt kort om jag inte deltar i händelsen.",
"card_details": "Kortuppgifter",
"something_went_wrong_on_our_end": "Något gick fel från vår sida. Kontakta vårt supportteam så fixar vi det åt dig direkt.",
"please_provide_following_text_to_suppport": "Ange följande text när du kontaktar supporten så att vi kan hjälpa dig bättre",
"seats_and_no_show_fee_error": "För närvarande går det inte att aktivera platser och ta ut en avgift för utebliven närvaro",
"complete_your_booking": "Slutför din bokning",
"complete_your_booking_subject": "Slutför din bokning: {{title}} {{date}}",
@@ -1864,6 +1902,9 @@
"open_dialog_with_element_click": "Öppna din Cal-dialogruta när någon klickar på ett element.",
"need_help_embedding": "Behöver du hjälp? Se våra guider för inbäddning av Cal på Wix, Squarespace eller WordPress, kolla in våra vanliga frågor eller utforska avancerade inbäddningsalternativ.",
"book_my_cal": "Boka i min Cal",
"first_name": "Förnamn",
"last_name": "Efternamn",
"first_last_name": "Förnamn, Efternamn",
"invite_as": "Bjud in som",
"form_updated_successfully": "Formuläret har uppdaterats.",
"disable_attendees_confirmation_emails": "Inaktivera förinställda e-postbekräftelser för deltagare",
@@ -1877,6 +1918,7 @@
"first_event_type_webhook_description": "Skapa din första webhook för denna händelsetyp",
"install_app_on": "Installera appen på",
"create_for": "Skapa för",
"currency": "Valuta",
"organization_banner_description": "Skapa miljöer där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
"organization_banner_title": "Hantera organisationer med flera team",
"set_up_your_organization": "Konfigurera din organisation",
@@ -1937,6 +1979,79 @@
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Användare: {{userName}}",
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
"location_options": "{{locationCount}} platsalternativ",
"custom_plan": "Anpassad plan",
"email_embed": "E-post inbäddad",
"add_times_to_your_email": "Välj några tillgängliga tider och bädda in dem i din e-post",
"select_time": "Välj tid",
"select_date": "Välj datum",
"see_all_available_times": "Se alla tillgängliga tider",
"org_team_names_example": "t.ex. marknadsföringsteam",
"org_team_names_example_1": "t.ex. marknadsföringsteam",
"org_team_names_example_2": "t.ex. säljteam",
"org_team_names_example_3": "t.ex. designteam",
"org_team_names_example_4": "t.ex. ingenjörsteam",
"org_team_names_example_5": "t.ex. dataanalytikerteam",
"org_max_team_warnings": "Du kommer att kunna lägga till fler team senare.",
"what_is_this_meeting_about": "Vad handlar det här mötet om?",
"add_to_team": "Lägg till i teamet",
"remove_users_from_org": "Ta bort användare från organisationen",
"remove_users_from_org_confirm": "Är du säker på att du vill ta bort {{userCount}} användare från denna organisation?",
"user_has_no_schedules": "Den här användaren har inte skapat några scheman ännu",
"user_isnt_in_any_teams": "Den här användaren är inte med i något team",
"requires_booker_email_verification": "Kräver e-postverifiering för bokare",
"description_requires_booker_email_verification": "För att säkerställa bokarens e-postverifiering före schemaläggning av evenemang",
"requires_confirmation_mandatory": "Textmeddelanden kan endast skickas till deltagare när evenemangstypen kräver bekräftelse.",
"kyc_verification_information": "För att garantera säkerheten måste du verifiera ditt {{teamOrAccount}} innan du skickar textmeddelanden till deltagare. Kontakta oss på <a>{{supportEmail}}</a> och ange följande information:",
"kyc_verification_documents": "<ul><li>Din {{teamOrUser}}</li><li>För företag: Bifoga din dokumentation för verifiering av företag</li><li>För privatpersoner: Bifoga ett statligt utfärdat ID-kort</li></ul>",
"verify_team_or_account": "Verifiera {{teamOrAccount}}",
"verify_account": "Verifiera konto",
"kyc_verification": "KYC-verifiering",
"organizations": "Organisationer",
"org_admin_other_teams": "Andra team",
"org_admin_other_teams_description": "Här kan du se team i din organisation som du inte är en del av. Du kan lägga till dem om det behövs.",
"no_other_teams_found": "Inga andra team hittades",
"no_other_teams_found_description": "Det finns inga andra team i denna organisation.",
"attendee_first_name_variable": "Deltagarens förnamn",
"attendee_last_name_variable": "Deltagarens efternamn",
"attendee_first_name_info": "Förnamnet på personen som bokat",
"attendee_last_name_info": "Efternamnet på personen som bokat",
"me": "Jag",
"verify_team_tooltip": "Verifiera ditt team för att aktivera att skicka meddelanden till deltagare",
"member_removed": "Medlem borttagen",
"my_availability": "Min tillgänglighet",
"team_availability": "Teamets tillgänglighet",
"backup_code": "Säkerhetskopiera kod",
"backup_codes": "Säkerhetskopiera koder",
"backup_code_instructions": "Varje säkerhetskopierad kod kan användas exakt en gång för att ge åtkomst utan din autentiserare.",
"backup_codes_copied": "Säkerhetskopierade koder kopierade!",
"incorrect_backup_code": "Den säkerhetskopierade koden är felaktig.",
"lost_access": "Förlorad åtkomst",
"missing_backup_codes": "Inga säkerhetskopieringskoder hittades. Generera dem i dina inställningar.",
"admin_org_notification_email_subject": "Ny organisation skapad: väntande åtgärd",
"hi_admin": "Hej administratör",
"admin_org_notification_email_title": "En organisation kräver DNS-inställning",
"admin_org_notification_email_body_part1": "En organisation med slug \"{{orgSlug}}\" skapades.<br /><br />Se till att konfigurera ditt DNS-register så att underdomänen motsvarar den nya organisationen där huvudappen körs. Annars kommer organisationen inte att fungera.<br /><br />Här är bara de allra grundläggande alternativen för att konfigurera en underdomän för att peka på deras app så den laddar organisationens profilsida.<br /><br />Du kan göra det antingen med A-posten:",
"admin_org_notification_email_body_part2": "Eller CNAME-posten:",
"admin_org_notification_email_body_part3": "När du har konfigurerat underdomänen, markera DNS-konfigurationen som gjort i organisationens admin-inställningar.",
"admin_org_notification_email_cta": "Gå till organisationens admin-inställningar",
"org_has_been_processed": "Org har behandlats",
"org_error_processing": "Det har uppstått ett fel vid behandlingen av denna organisation",
"orgs_page_description": "En lista över alla organisationer. Genom att acceptera en organisation kan alla användare med den e-postdomänen registrera sig UTAN e-postverifiering.",
"unverified": "Overifierad",
"dns_missing": "DNS saknas",
"mark_dns_configured": "Markera som DNS konfigurerad",
"value": "Värde",
"your_organization_updated_sucessfully": "Din organisation har uppdaterats",
"team_no_event_types": "Detta team har inga händelsetyper",
"seat_options_doesnt_multiple_durations": "Platsalternativet stöder inte flera varaktigheter",
"include_calendar_event": "Inkludera kalenderhändelse",
"recently_added": "Nyligen tillagda",
"no_members_found": "Inga medlemmar hittades",
"event_setup_length_error": "Konfigurering av evenemang: Längden måste vara minst 1 minut.",
"availability_schedules": "Scheman för tillgänglighet",
"view_only_edit_availability_not_onboarded": "Den här användaren har inte slutfört introduktionen. Du kommer inte att kunna ange deras tillgänglighet förrän de har slutfört introduktionen.",
"view_only_edit_availability": "Du tittar på användarens tillgänglighet. Du kan bara redigera din egen tillgänglighet.",
"edit_users_availability": "Redigera användarens tillgänglighet: {{username}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+17 -18
View File
@@ -11,18 +11,17 @@ it("correctly handles 1 day", async () => {
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1
scheduleId: 1,
};
const result = availabilityAsString(availability, {
locale: 'en',
hour12: true
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon, 9:00 AM - 5:00 PM");
});
it("correctly handles all days", async () => {
const availability = {
id: 1,
@@ -32,12 +31,12 @@ it("correctly handles all days", async () => {
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1
scheduleId: 1,
};
const result = availabilityAsString(availability, {
locale: 'en',
hour12: true
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Sun, 9:00 AM - 5:00 PM");
@@ -52,12 +51,12 @@ it("correctly handles staggered days", async () => {
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1
scheduleId: 1,
};
const result = availabilityAsString(availability, {
locale: 'en',
hour12: true
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon, Wed, Fri, Sun, 9:00 AM - 5:00 PM");
@@ -72,12 +71,12 @@ it("correctly produces days and times - 12 hours", async () => {
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1
scheduleId: 1,
};
const result = availabilityAsString(availability, {
locale: 'en',
hour12: true
locale: "en",
hour12: true,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 9:00 AM - 5:00 PM");
@@ -92,12 +91,12 @@ it("correctly produces days and times - 24 hours", async () => {
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
date: null,
scheduleId: 1
scheduleId: 1,
};
const result = availabilityAsString(availability, {
locale: 'en',
hour12: false
locale: "en",
hour12: false,
});
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 09:00 - 17:00");
@@ -107,5 +106,5 @@ it("correctly produces days and times - 24 hours", async () => {
// produces Unicode characters. Instead of using line for line code from the
// availability.ts file, opted for this instead.
const replaceUnicodeSpace = (string: string) => {
return string.replace(/\u202f/g, ' ');
}
return string.replace(/\u202f/g, " ");
};
+2 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import dayjs from "@calcom/dayjs";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkBookingLimits, checkBookingLimit } from "@calcom/lib/server";
@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it } from "vitest";
import dayjs from "@calcom/dayjs";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { checkDurationLimit, checkDurationLimits } from "@calcom/lib/server";
type MockData = {
id: number;
startDate: Date;
@@ -2,10 +2,9 @@ import { expect, it, beforeAll, vi } from "vitest";
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
})
});
const HAWAII_AND_NEWYORK_TEAM = [
{
@@ -4,10 +4,9 @@ import { expect, it, beforeAll, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
})
});
//parse "hh:mm-hh:mm" into <Availability> object
const parseWorkingHours = (workingHours: string) => {
+18 -19
View File
@@ -8,7 +8,8 @@ import prisma from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario";
import { getDate, getGoogleCalendarCredential, createBookingScenario } from "../utils/bookingScenario";
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
@@ -16,7 +17,7 @@ prismaMock.user.findMany.mockResolvedValue([]);
vi.mock("@calcom/lib/constants", () => ({
IS_PRODUCTION: true,
WEBAPP_URL: "http://localhost:3000"
WEBAPP_URL: "http://localhost:3000",
}));
declare global {
@@ -143,7 +144,6 @@ const TestData = {
},
};
const cleanup = async () => {
await prisma.eventType.deleteMany();
await prisma.user.deleteMany();
@@ -296,7 +296,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
// getSchedule returns timeslots in GMT
@@ -328,7 +328,7 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
@@ -393,7 +393,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventWith30Length).toHaveTimeSlots(
@@ -429,7 +429,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
// `slotInterval` takes precedence over `length`
// 4:30 is utc so it is 10:00 in IST
@@ -493,7 +493,7 @@ describe("getSchedule", () => {
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
[
@@ -514,7 +514,7 @@ describe("getSchedule", () => {
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
[
@@ -578,7 +578,7 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
@@ -652,7 +652,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
@@ -710,7 +710,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(schedule).toHaveTimeSlots(
@@ -774,7 +774,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
@@ -852,7 +852,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
},
});
expect(thisUserAvailability).toHaveTimeSlots(
@@ -950,7 +950,7 @@ describe("getSchedule", () => {
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
},
});
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
@@ -980,7 +980,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
},
});
// A user with blocked time in another event, still affects Team Event availability
@@ -1088,7 +1088,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
},
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
@@ -1116,7 +1116,7 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
},
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
@@ -1139,4 +1139,3 @@ describe("getSchedule", () => {
});
});
});
+1 -1
View File
@@ -5,7 +5,7 @@ import { getWorkingHours } from "@calcom/lib/availability";
beforeAll(() => {
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
})
});
it("correctly translates Availability (UTC+0) to UTC workingHours", async () => {
expect(
@@ -1,3 +1,5 @@
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import type { EventType } from "@prisma/client";
import { describe, expect, it, vi } from "vitest";
@@ -6,8 +8,6 @@ import { buildEventType } from "@calcom/lib/test/builder";
import type { Prisma } from "@calcom/prisma/client";
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
const mockFindFirstEventType = (data?: Partial<CompleteEventType>) => {
const eventType = buildEventType(data as Partial<EventType>);
prismaMock.eventType.findFirst.mockResolvedValue(eventType as EventType);
@@ -97,11 +97,12 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
@@ -133,11 +134,21 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, locations, parentId, userId, scheduleId,requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
const {
schedulingType,
id,
teamId,
timeZone,
locations,
parentId,
userId,
scheduleId,
requiresBookerEmailVerification,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
@@ -218,11 +229,12 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
@@ -255,11 +267,20 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, users, locations, parentId, userId,requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
const {
schedulingType,
id,
teamId,
timeZone,
locations,
parentId,
userId,
requiresBookerEmailVerification,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
});
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
+21 -37
View File
@@ -1,4 +1,5 @@
import { it, expect, describe, beforeAll } from "vitest";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -30,67 +31,50 @@ beforeAll(async () => {
describe("next.config.js - Org Rewrite", () => {
const orgHostRegExp = (subdomainRegExp: string) =>
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`);
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
it("https://app.cal.com", () => {
const subdomainRegExp = getSubdomainRegExp("https://app.cal.com");
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
expect(
orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups
?.orgSlug
).toEqual("company");
expect(
orgHostRegExp(subdomainRegExp).exec("org.cal.com")?.groups?.orgSlug
).toEqual("org");
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
expect(orgHostRegExp(subdomainRegExp).exec("org.cal.com")?.groups?.orgSlug).toEqual("org");
expect(
orgHostRegExp(subdomainRegExp).exec("localhost:3000")
).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("localhost:3000")).toEqual(null);
});
it("app.cal.com", () => {
const subdomainRegExp = getSubdomainRegExp("app.cal.com");
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
expect(
orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups
?.orgSlug
).toEqual("company");
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
});
it("https://calcom.app.company.com", () => {
const subdomainRegExp = getSubdomainRegExp(
"https://calcom.app.company.com"
const subdomainRegExp = getSubdomainRegExp("https://calcom.app.company.com");
expect(orgHostRegExp(subdomainRegExp).exec("calcom.app.company.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.app.company.com")?.groups?.orgSlug).toEqual(
"acme"
);
expect(
orgHostRegExp(subdomainRegExp).exec("calcom.app.company.com")
).toEqual(null);
expect(
orgHostRegExp(subdomainRegExp).exec("acme.calcom.app.company.com")
?.groups?.orgSlug
).toEqual("acme");
});
it("https://calcom.example.com", () => {
const subdomainRegExp = getSubdomainRegExp("https://calcom.example.com");
expect(orgHostRegExp(subdomainRegExp).exec("calcom.example.com")).toEqual(
null
);
expect(
orgHostRegExp(subdomainRegExp).exec("acme.calcom.example.com")?.groups
?.orgSlug
).toEqual("acme");
expect(orgHostRegExp(subdomainRegExp).exec("calcom.example.com")).toEqual(null);
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.example.com")?.groups?.orgSlug).toEqual("acme");
// The following also matches which causes anything other than the domain in NEXT_PUBLIC_WEBAPP_URL to give 404
expect(
orgHostRegExp(subdomainRegExp).exec("some-other.company.com")?.groups
?.orgSlug
).toEqual("some-other");
expect(orgHostRegExp(subdomainRegExp).exec("some-other.company.com")?.groups?.orgSlug).toEqual(
"some-other"
);
});
it("Should ignore Vercel preview URLs", () => {
const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app");
expect(orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
expect(
orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")
).toMatchInlineSnapshot("null");
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot(
"null"
);
});
});
+2 -2
View File
@@ -1,7 +1,7 @@
import { expect, it } from "vitest";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import { expect, it } from "vitest";
import { getLuckyUser } from "@calcom/lib/server";
import { buildUser } from "@calcom/lib/test/builder";
+12 -5
View File
@@ -1,3 +1,7 @@
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import type {
EventType as PrismaEventType,
User as PrismaUser,
@@ -17,10 +21,6 @@ import type { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
type App = {
slug: string;
dirName: string;
@@ -194,10 +194,17 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.OR[0].status?.in || [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const userIdIn = where.OR[0].userId?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
statusIn.includes(booking.status) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(booking.userId === where.OR[0].userId || userIdIn.includes(booking.userId));
// We return this booking if either condition is met
return firstConditionMatches;
+8
View File
@@ -0,0 +1,8 @@
---
items:
- iframe: { src: https://www.youtube.com/embed/tExtOJqnI0Q }
- 1.jpg
- 2.png
---
{DESCRIPTION}
+16
View File
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;
+1
View File
@@ -0,0 +1 @@
export { default as add } from "./add";
+17
View File
@@ -0,0 +1,17 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Alby",
"slug": "alby",
"type": "alby_payment",
"logo": "icon.svg",
"url": "https://example.com/link",
"variant": "payment",
"categories": ["payment"],
"publisher": "Alby",
"email": "support@getalby.com",
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"dirName": "alby"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/alby",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="554" height="554" viewBox="0 0 554 554" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="554" height="554" rx="64" fill="white"/>
<path d="M127.7 202.4C107.431 202.4 90.9999 185.969 90.9999 165.7C90.9999 145.431 107.431 129 127.7 129C147.969 129 164.4 145.431 164.4 165.7C164.4 185.969 147.969 202.4 127.7 202.4Z" fill="black"/>
<path d="M121.6 160.2L190.1 228.7" stroke="black" stroke-width="18.3"/>
<path d="M427.2 202.4C447.469 202.4 463.9 185.969 463.9 165.7C463.9 145.431 447.469 129 427.2 129C406.931 129 390.5 145.431 390.5 165.7C390.5 185.969 406.931 202.4 427.2 202.4Z" fill="black"/>
<path d="M434 160.2L365.5 228.7" stroke="black" stroke-width="18.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M138.1 384.6C128.451 380.122 120.543 372.597 115.592 363.182C110.641 353.768 108.922 342.987 110.7 332.5C127.3 240 196.2 170.6 278.7 170.6C361.4 170.6 430.5 240.3 446.8 333.2C448.539 343.695 446.779 354.47 441.792 363.867C436.805 373.263 428.866 380.759 419.2 385.2C375.441 405.799 327.664 416.454 279.3 416.4C228.8 416.4 180.9 405 138.1 384.6Z" fill="#FFDF6F"/>
<path d="M119.8 334.2C135.8 244.7 201.8 179.8 278.8 179.8V161.5C190.6 161.5 118.8 235.3 101.7 330.9L119.7 334.1L119.8 334.2ZM278.8 179.8C355.8 179.8 422 245.1 437.8 334.8L455.8 331.6C439 235.6 367 161.5 278.8 161.5V179.8ZM415.3 376.9C372.76 396.917 326.314 407.265 279.3 407.2V425.5C330.7 425.5 379.4 414 423.1 393.5L415.3 376.9ZM279.3 407.2C230.2 407.2 183.7 396.1 142.1 376.3L134.2 392.9C178.2 413.9 227.4 425.5 279.3 425.5V407.2ZM437.8 334.8C440.8 352.1 431.6 369.3 415.3 376.9L423.1 393.5C446.5 382.5 460.4 357.5 455.8 331.5L437.8 334.8ZM101.7 330.8C99.5558 343.269 101.577 356.098 107.451 367.303C113.325 378.509 122.725 387.47 134.2 392.8L142.1 376.3C134.267 372.688 127.84 366.599 123.81 358.973C119.78 351.347 118.371 342.606 119.8 334.1L101.6 330.9L101.7 330.8Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M173.4 361.6C157.5 355.1 148.1 338.2 153.6 321.9C170.6 271.7 220.2 235.4 278.7 235.4C337.2 235.4 386.8 271.7 403.8 321.9C409.4 338.2 399.9 355.1 384 361.6C350.559 375.19 314.796 382.153 278.7 382.1C241.5 382.1 205.9 374.8 173.4 361.6Z" fill="black"/>
<path d="M320.9 338.1C337.8 338.1 351.5 327.176 351.5 313.7C351.5 300.224 337.8 289.3 320.9 289.3C304 289.3 290.3 300.224 290.3 313.7C290.3 327.176 304 338.1 320.9 338.1Z" fill="white"/>
<path d="M233.4 338.1C250.3 338.1 264 327.176 264 313.7C264 300.224 250.3 289.3 233.4 289.3C216.5 289.3 202.8 300.224 202.8 313.7C202.8 327.176 216.5 338.1 233.4 338.1Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -16,6 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: req.session?.user?.id,
},
select: {
email: true,
id: true,
},
});
@@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dav = new CalendarService({
id: 0,
...data,
user: { email: user.email },
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -2,6 +2,7 @@
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import alby_config_json from "./alby/config.json";
import amie_config_json from "./amie/config.json";
import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata";
import around_config_json from "./around/config.json";
@@ -73,6 +74,7 @@ import zohocrm_config_json from "./zohocrm/config.json";
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
export const appStoreMetadata = {
alby: alby_config_json,
amie: amie_config_json,
applecalendar: applecalendar__metadata_ts,
around: around_config_json,
@@ -3,6 +3,7 @@
Don't modify this file manually.
**/
export const apiHandlers = {
alby: import("./alby/api"),
amie: import("./amie/api"),
applecalendar: import("./applecalendar/api"),
around: import("./around/api"),
@@ -1,4 +1,5 @@
import type { PrismaClient } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -24,6 +25,7 @@ export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHand
where: {
userId: user?.id,
},
select: credentialForCalendarServiceSelect,
});
if (!credential) {
@@ -1,4 +1,5 @@
import type { PrismaClient } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -20,6 +21,7 @@ export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => {
where: {
userId: user?.id,
},
select: credentialForCalendarServiceSelect,
});
if (!credential) {
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
@@ -17,6 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
select: {
id: true,
email: true,
},
});
@@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dav = new CalendarService({
id: 0,
...data,
user: { email: user.email },
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -60,6 +60,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = {
type: "daily_video",
key: { apikey: process.env.DAILY_API_KEY },
userId: 0,
user: { email: "" },
appId: "daily-video",
invalid: false,
teamId: null,
@@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
select: {
id: true,
email: true,
},
});
@@ -42,6 +43,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const dav = new CalendarService({
id: 0,
...data,
user: { email: user.email },
});
await dav?.listCalendars();
await prisma.credential.create({
@@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
select: {
id: true,
email: true,
},
});
@@ -41,6 +42,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
try {
const dav = new CalendarService({
id: 0,
user: { email: user.email },
...data,
});
await dav?.listCalendars();
@@ -36,7 +36,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
};
try {
const service = new CalendarService({ id: 0, ...data });
const service = new CalendarService({ id: 0, user: { email: session.user.email || "" }, ...data });
await service?.listCalendars();
await prisma.credential.create({ data });
} catch (reason) {
@@ -28,11 +28,13 @@ export default class GoogleCalendarService implements Calendar {
private integrationName = "";
private auth: { getToken: () => Promise<MyGoogleAuth> };
private log: typeof logger;
private credential: CredentialPayload;
constructor(credential: CredentialPayload) {
this.integrationName = "google_calendar";
this.auth = this.googleAuth(credential);
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
this.credential = credential;
}
private googleAuth = (credential: CredentialPayload) => {
@@ -84,22 +86,50 @@ export default class GoogleCalendarService implements Calendar {
};
};
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
private getAttendees = (event: CalendarEvent) => {
// When rescheduling events we know the external id of the calendar so we can just look for it in the destinationCalendar array.
const selectedHostDestinationCalendar = event.destinationCalendar?.find(
(cal) => cal.credentialId === this.credential.id
);
const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({
...rest,
responseStatus: "accepted",
}));
// TODO: Check every other CalendarService for team members
const teamMembers =
calEventRaw.team?.members.map((m) => ({
email: m.email,
displayName: m.name,
const attendees: calendar_v3.Schema$EventAttendee[] = [
{
...event.organizer,
id: String(event.organizer.id),
responseStatus: "accepted",
})) || [];
organizer: true,
// Tried changing the display name to the user but GCal will not let you do that. It will only display the name of the external calendar. Leaving this in just incase it works in the future.
displayName: event.organizer.name,
email: selectedHostDestinationCalendar?.externalId ?? event.organizer.email,
},
...eventAttendees,
];
if (event.team?.members) {
// TODO: Check every other CalendarService for team members
const teamAttendeesWithoutCurrentUser = event.team.members
.filter((member) => member.email !== this.credential.user?.email)
.map((m) => {
const teamMemberDestinationCalendar = event.destinationCalendar?.find(
(calendar) => calendar.integration === "google_calendar" && calendar.userId === m.id
);
return {
email: teamMemberDestinationCalendar?.externalId ?? m.email,
displayName: m.name,
responseStatus: "accepted",
};
});
attendees.push(...teamAttendeesWithoutCurrentUser);
}
return attendees;
};
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
return new Promise(async (resolve, reject) => {
const selectedHostDestinationCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
);
const myGoogleAuth = await this.auth.getToken();
const payload: calendar_v3.Schema$Event = {
summary: calEventRaw.title,
@@ -112,19 +142,7 @@ export default class GoogleCalendarService implements Calendar {
dateTime: calEventRaw.endTime,
timeZone: calEventRaw.organizer.timeZone,
},
attendees: [
{
...calEventRaw.organizer,
id: String(calEventRaw.organizer.id),
responseStatus: "accepted",
organizer: true,
email: selectedHostDestinationCalendar?.externalId
? selectedHostDestinationCalendar.externalId
: calEventRaw.organizer.email,
},
...eventAttendees,
...teamMembers,
],
attendees: this.getAttendees(calEventRaw),
reminders: {
useDefault: true,
},
@@ -194,19 +212,7 @@ export default class GoogleCalendarService implements Calendar {
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
return new Promise(async (resolve, reject) => {
const [mainHostDestinationCalendar] =
event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : [];
const myGoogleAuth = await this.auth.getToken();
const eventAttendees = event.attendees.map(({ ...rest }) => ({
...rest,
responseStatus: "accepted",
}));
const teamMembers =
event.team?.members.map((m) => ({
email: m.email,
displayName: m.name,
responseStatus: "accepted",
})) || [];
const payload: calendar_v3.Schema$Event = {
summary: event.title,
description: getRichDescription(event),
@@ -218,19 +224,7 @@ export default class GoogleCalendarService implements Calendar {
dateTime: event.endTime,
timeZone: event.organizer.timeZone,
},
attendees: [
{
...event.organizer,
id: String(event.organizer.id),
organizer: true,
responseStatus: "accepted",
email: mainHostDestinationCalendar?.externalId
? mainHostDestinationCalendar.externalId
: event.organizer.email,
},
...(eventAttendees as any),
...(teamMembers as any),
],
attendees: this.getAttendees(event),
reminders: {
useDefault: true,
},
@@ -34,11 +34,13 @@ export default class LarkCalendarService implements Calendar {
private integrationName = "";
private log: typeof logger;
auth: { getToken: () => Promise<string> };
private credential: CredentialPayload;
constructor(credential: CredentialPayload) {
this.integrationName = "lark_calendar";
this.auth = this.larkAuth(credential);
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
this.credential = credential;
}
private larkAuth = (credential: CredentialPayload) => {
@@ -122,10 +124,13 @@ export default class LarkCalendarService implements Calendar {
});
};
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === this.credential.id) ??
event.destinationCalendar[0]
: undefined;
const calendarId = mainHostDestinationCalendar?.externalId;
if (!calendarId) {
throw new Error("no calendar id");
@@ -143,7 +148,7 @@ export default class LarkCalendarService implements Calendar {
}
try {
await this.createAttendees(event, eventId);
await this.createAttendees(event, eventId, credentialId);
return {
...eventRespData,
uid: eventRespData.data.event.event_id as string,
@@ -160,8 +165,11 @@ export default class LarkCalendarService implements Calendar {
}
}
private createAttendees = async (event: CalendarEvent, eventId: string) => {
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
private createAttendees = async (event: CalendarEvent, eventId: string, credentialId: number) => {
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0]
: undefined;
const calendarId = mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in createAttendees");
@@ -189,7 +197,9 @@ export default class LarkCalendarService implements Calendar {
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const mainHostDestinationCalendar = event.destinationCalendar?.find(
(cal) => cal.externalId === externalCalendarId
);
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in updateEvent");
@@ -234,7 +244,9 @@ export default class LarkCalendarService implements Calendar {
* @returns
*/
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const mainHostDestinationCalendar = event.destinationCalendar?.find(
(cal) => cal.externalId === externalCalendarId
);
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in deleteEvent");
@@ -393,13 +405,16 @@ export default class LarkCalendarService implements Calendar {
attendeeArray.push(attendee);
});
event.team?.members.forEach((member) => {
const attendee: LarkEventAttendee = {
type: "third_party",
is_optional: false,
third_party_email: member.email,
};
attendeeArray.push(attendee);
if (member.email !== this.credential.user?.email) {
const attendee: LarkEventAttendee = {
type: "third_party",
is_optional: false,
third_party_email: member.email,
};
attendeeArray.push(attendee);
}
});
return attendeeArray;
};
}
@@ -61,16 +61,20 @@ export default class Office365CalendarService implements Calendar {
private accessToken: string | null = null;
auth: { getToken: () => Promise<string> };
private apiGraphUrl = "https://graph.microsoft.com/v1.0";
private credential: CredentialPayload;
constructor(credential: CredentialPayload) {
this.integrationName = "office365_calendar";
this.auth = this.o365Auth(credential);
this.credential = credential;
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0]
: undefined;
try {
const eventsUrl = mainHostDestinationCalendar?.externalId
? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events`
@@ -295,6 +299,16 @@ export default class Office365CalendarService implements Calendar {
timeZone: event.organizer.timeZone,
},
attendees: [
// Add the calEvent organizer
{
emailAddress: {
address: event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
event.organizer.email
: event.organizer.email,
name: event.organizer.name,
},
},
...event.attendees.map((attendee) => ({
emailAddress: {
address: attendee.email,
@@ -303,13 +317,20 @@ export default class Office365CalendarService implements Calendar {
type: "required",
})),
...(event.team?.members
? event.team?.members.map((member) => ({
emailAddress: {
address: member.email,
name: member.name,
},
type: "required",
}))
? event.team?.members
.filter((member) => member.email !== this.credential.user?.email)
.map((member) => {
const destinationCalendar =
event.destinationCalendar &&
event.destinationCalendar.find((cal) => cal.userId === member.id);
return {
emailAddress: {
address: destinationCalendar?.externalId ?? member.email,
name: member.name,
},
type: "required",
};
})
: []),
],
location: event.location ? { displayName: getLocation(event) } : undefined,
@@ -454,12 +475,13 @@ export default class Office365CalendarService implements Calendar {
(acc: BufferedBusyTime[], subResponse: { body: { value?: BodyValue[]; error?: Error[] } }) => {
if (!subResponse.body?.value) return acc;
return acc.concat(
subResponse.body.value
.filter((evt) => evt.showAs !== "free" && evt.showAs !== "workingElsewhere")
.map((evt) => ({
subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => {
if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc;
return acc.concat({
start: evt.start.dateTime + "Z",
end: evt.end.dateTime + "Z",
}))
});
}, [])
);
},
[]
+3 -3
View File
@@ -53,7 +53,7 @@ class Paypal {
const { access_token, expires_in } = await response.json();
this.accessToken = access_token;
this.expiresAt = Date.now() + expires_in;
} else {
} else if (response?.status) {
console.error(`Request failed with status ${response.status}`);
}
} catch (error) {
@@ -127,7 +127,7 @@ class Paypal {
});
if (captureResult.ok) {
const result = await captureResult.json();
if (result.body.status === "COMPLETED") {
if (result?.status === "COMPLETED") {
// Get payment reference id
const payment = await prisma.payment.findFirst({
@@ -153,7 +153,7 @@ class Paypal {
success: true,
data: Object.assign(
{},
{ ...(payment?.data as Record<string, string | number>), capture: result.body.id }
{ ...(payment?.data as Record<string, string | number>), capture: result.id }
) as unknown as Prisma.InputJsonValue,
},
});
@@ -1,9 +1,11 @@
import Head from "next/head";
import { stringify } from "querystring";
import z from "zod";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import logger from "@calcom/lib/logger";
import { TRPCError } from "@calcom/trpc";
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -92,11 +94,22 @@ export const getServerSideProps = async function getServerSideProps(
const { default: trpcRouter } = await import("@calcom/app-store/routing-forms/trpc/_router");
const caller = trpcRouter.createCaller(ctx);
const { v4: uuidv4 } = await import("uuid");
await caller.public.response({
formId: form.id,
formFillerId: uuidv4(),
response: response,
});
try {
await caller.public.response({
formId: form.id,
formFillerId: uuidv4(),
response: response,
});
} catch (e) {
if (e instanceof TRPCError) {
return {
props: {
form: serializableForm,
message: e.message,
},
};
}
}
//TODO: Maybe take action after successful mutation
if (decidedAction.type === "customPageMessage") {
@@ -109,14 +122,14 @@ export const getServerSideProps = async function getServerSideProps(
} else if (decidedAction.type === "eventTypeRedirectUrl") {
return {
redirect: {
destination: `/${decidedAction.value}`,
destination: `/${decidedAction.value}?${stringify(context.query)}`,
permanent: false,
},
};
} else if (decidedAction.type === "externalRedirectUrl") {
return {
redirect: {
destination: `${decidedAction.value}`,
destination: `${decidedAction.value}?${stringify(context.query)}`,
permanent: false,
},
};
@@ -304,12 +304,14 @@ test.describe("Routing Forms", () => {
await users.logout();
page.goto(`/router?form=${routingForm.id}&Test field=event-routing`);
await page.waitForURL((url) => {
return url.pathname.endsWith("/pro/30min");
return url.pathname.endsWith("/pro/30min") && url.searchParams.get("Test field") === "event-routing";
});
page.goto(`/router?form=${routingForm.id}&Test field=external-redirect`);
await page.waitForURL((url) => {
return url.hostname.includes("google.com");
return (
url.hostname.includes("google.com") && url.searchParams.get("Test field") === "external-redirect"
);
});
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
@@ -73,12 +73,14 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) =>
}
return !schema.safeParse(fieldValue).success;
})
.map((f) => ({ label: f.label, type: f.type }));
.map((f) => ({ label: f.label, type: f.type, value: response[f.id]?.value }));
if (invalidFields.length) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invalid fields ${invalidFields.map((f) => `${f.label}: ${f.type}`)}`,
message: `Invalid value for fields ${invalidFields
.map((f) => `'${f.label}' with value '${f.value}' should be valid ${f.type}`)
.join(", ")}`,
});
}
+2 -7
View File
@@ -5,6 +5,7 @@ import { defaultVideoAppCategories } from "@calcom/app-store/utils";
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
import { prisma } from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { defaultLocations } from "./locations";
@@ -62,13 +63,7 @@ export async function getLocationGroupedOptions(
},
},
select: {
id: true,
type: true,
key: true,
userId: true,
teamId: true,
appId: true,
invalid: true,
...credentialForCalendarServiceSelect,
team: {
select: {
name: true,
@@ -49,9 +49,13 @@ export class PaymentService implements IAbstractPaymentService {
async create(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: Booking["id"],
userId: Booking["userId"],
username: string | null,
bookerName: string,
bookerEmail: string,
paymentOption: PaymentOption,
eventTitle?: string
eventTitle?: string,
bookingTitle?: string
) {
try {
// Ensure that the payment service can support the passed payment option
@@ -82,8 +86,12 @@ export class PaymentService implements IAbstractPaymentService {
metadata: {
identifier: "cal.com",
bookingId,
calAccountId: userId,
calUsername: username,
bookerName,
bookerEmail,
eventName: eventTitle || "",
eventTitle: eventTitle || "",
bookingTitle: bookingTitle || "",
},
};
+3 -8
View File
@@ -1,11 +1,11 @@
import type { AppCategories } from "@prisma/client";
import { Prisma } from "@prisma/client";
// If you import this file on any app it should produce circular dependency
// import appStore from "./index";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { App, AppMeta } from "@calcom/types/App";
import type { CredentialPayload } from "@calcom/types/Credential";
export * from "./_utils/getEventTypeAppData";
@@ -30,13 +30,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
return store;
}, {} as Record<string, AppMeta>);
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
select: { id: true, type: true, key: true, userId: true, teamId: true, appId: true, invalid: true },
});
export type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
export type CredentialDataWithTeamName = CredentialData & {
export type CredentialDataWithTeamName = CredentialPayload & {
team?: {
name: string;
} | null;
@@ -64,6 +58,7 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: appMeta.key!,
userId: 0,
user: { email: "" },
teamId: null,
appId: appMeta.slug,
invalid: false,
+3
View File
@@ -8,6 +8,9 @@ module.exports = {
printWidth: 110,
arrowParens: "always",
importOrder: [
// Mocks must be at the top as they contain vi.mock calls
"(.*)/__mocks__/(.*)",
"<THIRD_PARTY_MODULES>",
"^@(calcom|ee)/(.*)$",
"^@lib/(.*)$",
"^@components/(.*)$",
+5 -5
View File
@@ -14,7 +14,7 @@ import type {
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { EventResult } from "@calcom/types/EventManager";
import getCalendarsEvents from "./getCalendarsEvents";
@@ -216,7 +216,7 @@ export const getBusyCalendarTimes = async (
};
export const createEvent = async (
credential: CredentialWithAppName,
credential: CredentialPayload,
calEvent: CalendarEvent,
externalId?: string
): Promise<EventResult<NewCalendarEventType>> => {
@@ -255,7 +255,7 @@ export const createEvent = async (
: undefined;
return {
appName: credential.appName,
appName: credential.appId || "",
type: credential.type,
success,
uid,
@@ -270,7 +270,7 @@ export const createEvent = async (
};
export const updateEvent = async (
credential: CredentialWithAppName,
credential: CredentialPayload,
calEvent: CalendarEvent,
bookingRefUid: string | null,
externalCalendarId: string | null
@@ -311,7 +311,7 @@ export const updateEvent = async (
}
return {
appName: credential.appName,
appName: credential.appId || "",
type: credential.type,
success,
uid,
+43 -32
View File
@@ -1,4 +1,4 @@
import type { DestinationCalendar, Booking } from "@prisma/client";
import type { Booking, DestinationCalendar } from "@prisma/client";
// eslint-disable-next-line no-restricted-imports
import { cloneDeep, merge } from "lodash";
import { v5 as uuidv5 } from "uuid";
@@ -7,18 +7,21 @@ import type { z } from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvideo/zod";
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import { MeetLocationType } from "@calcom/app-store/locations";
import { getEventLocationTypeFromApp, MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event";
import type { EventResult } from "@calcom/types/EventManager";
import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
import type {
CreateUpdateResult,
EventResult,
PartialBooking,
PartialReference,
} from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient";
@@ -68,8 +71,8 @@ export type EventManagerUser = {
type createdEventSchema = z.infer<typeof createdEventSchema>;
export default class EventManager {
calendarCredentials: CredentialWithAppName[];
videoCredentials: CredentialWithAppName[];
calendarCredentials: CredentialPayload[];
videoCredentials: CredentialPayload[];
/**
* Takes an array of credentials and initializes a new instance of the EventManager.
@@ -262,6 +265,7 @@ export default class EventManager {
select: {
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
},
},
},
@@ -337,26 +341,37 @@ export default class EventManager {
private async createAllCalendarEvents(event: CalendarEvent) {
let createdEvents: EventResult<NewCalendarEventType>[] = [];
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
for (const destination of event.destinationCalendar) {
// Since GCal pushes events to multiple calendars we only want to create one event per booking
let gCalAdded = false;
const destinationCalendars: DestinationCalendar[] = event.destinationCalendar.reduce(
(destinationCals, cal) => {
if (cal.integration === "google_calendar") {
if (gCalAdded) {
return destinationCals;
} else {
gCalAdded = true;
destinationCals.push(cal);
}
} else {
destinationCals.push(cal);
}
return destinationCals;
},
[] as DestinationCalendar[]
);
for (const destination of destinationCalendars) {
if (destination.credentialId) {
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
if (!credential) {
// Fetch credential from DB
const credentialFromDB = await prisma.credential.findUnique({
include: {
app: {
select: {
slug: true,
},
},
},
where: {
id: destination.credentialId,
},
select: credentialForCalendarServiceSelect,
});
if (credentialFromDB && credentialFromDB.app?.slug) {
if (credentialFromDB && credentialFromDB.appId) {
credential = {
appName: credentialFromDB?.app.slug ?? "",
id: credentialFromDB.id,
type: credentialFromDB.type,
key: credentialFromDB.key,
@@ -364,6 +379,7 @@ export default class EventManager {
teamId: credentialFromDB.teamId,
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
user: credentialFromDB.user,
};
}
}
@@ -415,7 +431,7 @@ export default class EventManager {
* @private
*/
private getVideoCredential(event: CalendarEvent): CredentialWithAppName | undefined {
private getVideoCredential(event: CalendarEvent): CredentialPayload | undefined {
if (!event.location) {
return undefined;
}
@@ -443,7 +459,7 @@ export default class EventManager {
event.location +
" because credential is missing for the app"
);
videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
videoCredential = { ...FAKE_DAILY_CREDENTIAL };
}
return videoCredential;
@@ -523,20 +539,13 @@ export default class EventManager {
if (!credential) {
// Fetch credential from DB
const credentialFromDB = await prisma.credential.findUnique({
include: {
app: {
select: {
slug: true,
},
},
},
where: {
id: reference.credentialId,
},
select: credentialForCalendarServiceSelect,
});
if (credentialFromDB && credentialFromDB.app?.slug) {
if (credentialFromDB && credentialFromDB.appId) {
credential = {
appName: credentialFromDB?.app.slug ?? "",
id: credentialFromDB.id,
type: credentialFromDB.type,
key: credentialFromDB.key,
@@ -544,6 +553,7 @@ export default class EventManager {
teamId: credentialFromDB.teamId,
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
user: credentialFromDB.user,
};
}
}
@@ -566,6 +576,7 @@ export default class EventManager {
where: {
id: oldCalendarEvent.credentialId,
},
select: credentialForCalendarServiceSelect,
});
const calendar = await getCalendar(calendarCredential);
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
@@ -581,7 +592,7 @@ export default class EventManager {
if (!calendarReference) {
return {
appName: cred.appName,
appName: cred.appId || "",
type: cred.type,
success: false,
uid: "",
+6 -2
View File
@@ -57,8 +57,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
if (variable === bookingField) {
let fieldValue;
if (eventNameObj.bookingFields) {
fieldValue =
eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields]?.toString();
const field = eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields];
if (field && typeof field === "object" && "value" in field) {
fieldValue = field?.value?.toString();
} else {
fieldValue = field?.toString();
}
}
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
}
+15 -7
View File
@@ -22,25 +22,33 @@ export const getAggregatedAvailability = (
return mergeOverlappingDateRanges(availability);
};
function isSameDay(date1: Date, date2: Date) {
return (
date1.getUTCFullYear() === date2.getUTCFullYear() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCDate() === date2.getUTCDate()
);
}
function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
const sortedDateRanges = dateRanges.sort((a, b) => a.start.diff(b.start)); //is it already sorted before?
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const mergedDateRanges: DateRange[] = [];
let currentRange = sortedDateRanges[0];
let currentRange = dateRanges[0];
if (!currentRange) {
return [];
}
for (let i = 1; i < sortedDateRanges.length; i++) {
const nextRange = sortedDateRanges[i];
for (let i = 1; i < dateRanges.length; i++) {
const nextRange = dateRanges[i];
if (
currentRange.start.utc().format("DD MM YY") === nextRange.start.utc().format("DD MM YY") &&
currentRange.end.isAfter(nextRange.start)
isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) &&
currentRange.end.valueOf() > nextRange.start.valueOf()
) {
currentRange = {
start: currentRange.start,
end: currentRange.end.isAfter(nextRange.end) ? currentRange.end : nextRange.end,
end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
};
} else {
mergedDateRanges.push(currentRange);
+61 -48
View File
@@ -1,4 +1,4 @@
import type { Credential } from "@prisma/client";
import type { Booking, EventType } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import dayjs from "@calcom/dayjs";
@@ -9,10 +9,12 @@ import prisma from "@calcom/prisma";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDetails } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
export async function getBusyTimes(params: {
credentials: Credential[];
credentials: CredentialPayload[];
userId: number;
userEmail: string;
username: string;
eventTypeId?: number;
startTime: string;
@@ -23,10 +25,22 @@ export async function getBusyTimes(params: {
seatedEvent?: boolean;
rescheduleUid?: string | null;
duration?: number | null;
currentBookings?:
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
eventType: Pick<
EventType,
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
> | null;
_count?: {
seatsReferences: number;
};
})[]
| null;
}) {
const {
credentials,
userId,
userEmail,
username,
eventTypeId,
startTime,
@@ -38,6 +52,7 @@ export async function getBusyTimes(params: {
rescheduleUid,
duration,
} = params;
logger.silly(
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
userId,
@@ -45,15 +60,6 @@ export async function getBusyTimes(params: {
status: BookingStatus.ACCEPTED,
})}`
);
// get user email for attendee checking.
const user = await prisma.user.findUniqueOrThrow({
where: {
id: userId,
},
select: {
email: true,
},
});
/**
* A user is considered busy within a given time period if there
@@ -83,49 +89,56 @@ export async function getBusyTimes(params: {
in: [BookingStatus.ACCEPTED],
},
};
// Find bookings that block this user from hosting further bookings.
const bookings = await prisma.booking.findMany({
where: {
OR: [
// User is primary host (individual events, or primary organizer)
{
...sharedQuery,
userId,
},
// The current user has a different booking at this time he/she attends
{
...sharedQuery,
attendees: {
some: {
email: user.email,
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
// This function is called from multiple places but we aren't refactoring all of them at this moment
// to avoid potential side effects.
const bookings = params.currentBookings
? params.currentBookings
: await prisma.booking.findMany({
where: {
OR: [
// User is primary host (individual events, or primary organizer)
{
...sharedQuery,
userId,
},
},
// The current user has a different booking at this time he/she attends
{
...sharedQuery,
attendees: {
some: {
email: userEmail,
},
},
},
],
},
],
},
select: {
id: true,
uid: true,
startTime: true,
endTime: true,
title: true,
eventType: {
select: {
id: true,
afterEventBuffer: true,
beforeEventBuffer: true,
seatsPerTimeSlot: true,
},
},
...(seatedEvent && {
_count: {
select: {
seatsReferences: true,
uid: true,
userId: true,
startTime: true,
endTime: true,
title: true,
eventType: {
select: {
id: true,
afterEventBuffer: true,
beforeEventBuffer: true,
seatsPerTimeSlot: true,
},
},
...(seatedEvent && {
_count: {
select: {
seatsReferences: true,
},
},
}),
},
}),
},
});
});
const bookingSeatCountMap: { [x: string]: number } = {};
const busyTimes = bookings.reduce(
+16 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import type { Booking, Prisma, EventType as PrismaEventType } from "@prisma/client";
import { z } from "zod";
import type { Dayjs } from "@calcom/dayjs";
@@ -14,6 +14,7 @@ import { performance } from "@calcom/lib/server/perfObserver";
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
import type {
EventBusyDate,
@@ -81,7 +82,9 @@ const getUser = (where: Prisma.UserWhereInput) =>
where,
select: {
...availabilityUserSelect,
credentials: true,
credentials: {
select: credentialForCalendarServiceSelect,
},
},
});
@@ -128,6 +131,15 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
eventType?: EventType;
currentSeats?: CurrentSeats;
rescheduleUid?: string | null;
currentBookings?: (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
eventType: Pick<
PrismaEventType,
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
> | null;
_count?: {
seatsReferences: number;
};
})[];
}
) {
const { username, userId, dateFrom, dateTo, eventTypeId, afterEventBuffer, beforeEventBuffer, duration } =
@@ -180,6 +192,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
endTime: getBusyTimesEnd,
eventTypeId,
userId: user.id,
userEmail: user.email,
username: `${user.username}`,
beforeEventBuffer,
afterEventBuffer,
@@ -187,6 +200,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
seatedEvent: !!eventType?.seatsPerTimeSlot,
rescheduleUid: initialData?.rescheduleUid || null,
duration,
currentBookings: initialData?.currentBookings,
});
const detailedBusyTimes: EventBusyDetails[] = [
+9 -6
View File
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { GetRecordingsResponseSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoApiAdapterFactory, VideoCallData } from "@calcom/types/VideoApiAdapter";
@@ -48,7 +48,7 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[])
);
const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => {
const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => {
const uid: string = getUid(calEvent);
if (!credential || !credential.appId) {
@@ -69,7 +69,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
createdEvent: VideoCallData | undefined;
credentialId: number;
} = {
appName: credential.appName,
appName: credential.appId || "",
type: credential.type,
uid,
originalEvent: calEvent,
@@ -110,7 +110,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
};
const updateMeeting = async (
credential: CredentialWithAppName,
credential: CredentialPayload,
calEvent: CalendarEvent,
bookingRef: PartialReference | null
): Promise<EventResult<VideoCallData>> => {
@@ -131,7 +131,7 @@ const updateMeeting = async (
if (!updatedMeeting) {
return {
appName: credential.appName,
appName: credential.appId || "",
type: credential.type,
success,
uid,
@@ -140,7 +140,7 @@ const updateMeeting = async (
}
return {
appName: credential.appName,
appName: credential.appId || "",
type: credential.type,
success,
uid,
@@ -176,6 +176,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
appId: "daily-video",
type: "daily_video",
userId: null,
user: { email: "" },
teamId: null,
key: dailyAppKeys,
invalid: false,
@@ -200,6 +201,7 @@ const getRecordingsOfCalVideoByRoomName = async (
appId: "daily-video",
type: "daily_video",
userId: null,
user: { email: "" },
teamId: null,
key: dailyAppKeys,
invalid: false,
@@ -222,6 +224,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => {
appId: "daily-video",
type: "daily_video",
userId: null,
user: { email: "" },
teamId: null,
key: dailyAppKeys,
invalid: false,
@@ -1,4 +1,4 @@
import type { EmbedThemeConfig } from "./embed-iframe";
import type { EmbedThemeConfig } from "./src/types";
export default function EmbedInitIframe() {
if (typeof window === "undefined" || window.isEmbed) {
@@ -2,6 +2,8 @@ import { expect } from "@playwright/test";
import { test } from "@calcom/web/playwright/lib/fixtures";
import "../../src/types";
test.describe("Embed Pages", () => {
test("Event Type Page: should not have margin top on embed page", async ({ page }) => {
await page.goto("http://localhost:3000/free/30min/embed");
@@ -45,18 +47,77 @@ test.describe("Embed Pages", () => {
expect(marginFromTop).not.toBe(0);
});
test("should change to embed when window.name is changed to cal-embed=", async ({ page }) => {
await page.goto("http://localhost:3000/free/30min");
test.describe("isEmbed, getEmbedNamespace, getEmbedTheme testing", () => {
test("when `window.name` is set to 'cal-embed=' and `theme` is supplied as a query param", async ({
page,
}) => {
const queryParamTheme = "dark";
await page.evaluate(() => {
window.name = "cal-embed=";
});
await page.evaluate(() => {
window.name = "cal-embed=";
await page.goto(`http://localhost:3000/free/30min?theme=${queryParamTheme}`);
const isEmbed = await page.evaluate(() => {
return window?.isEmbed?.();
});
const embedNamespace = await page.evaluate(() => {
return window?.getEmbedNamespace?.();
});
expect(embedNamespace).toBe("");
expect(isEmbed).toBe(true);
const embedTheme = await page.evaluate(() => {
return window?.getEmbedTheme?.();
});
expect(embedTheme).toBe(queryParamTheme);
const embedStoreTheme = await page.evaluate(() => {
return window.CalEmbed.embedStore.theme;
});
// Verify that the theme is set on embedStore.
expect(embedStoreTheme).toBe(queryParamTheme);
});
await page.reload();
const isEmbed = await page.evaluate(() => {
return window?.isEmbed?.();
test("when `window.name` does not contain `cal-embed=`", async ({ page }) => {
await page.evaluate(() => {
window.name = "testing";
});
await page.goto(`http://localhost:3000/free/30min`);
const isEmbed = await page.evaluate(() => {
return window?.isEmbed?.();
});
const embedNamespace = await page.evaluate(() => {
return window?.getEmbedNamespace?.();
});
expect(isEmbed).toBe(false);
expect(embedNamespace).toBe(null);
});
test("`getEmbedTheme` should use `window.CalEmbed.embedStore.theme` instead of `theme` query param if set", async ({
page,
}) => {
const theme = "dark";
await page.evaluate(() => {
window.name = "cal-embed=";
});
await page.goto("http://localhost:3000/free/30min?theme=dark");
let embedTheme = await page.evaluate(() => {
return window?.getEmbedTheme?.();
});
expect(embedTheme).toBe(theme);
// Fake a scenario where theme query param is lost during navigation
await page.evaluate(() => {
history.pushState({}, "", "/free/30min");
});
embedTheme = await page.evaluate(() => {
return window?.getEmbedTheme?.();
});
// Theme should still remain same as it's read from `window.CalEmbed.embedStore.theme` which is updated by getEmbedTheme itself
expect(embedTheme).toBe(theme);
});
expect(isEmbed).toBe(true);
});
});
+10 -48
View File
@@ -1,29 +1,22 @@
import { useSearchParams } from "next/navigation";
import type { CSSProperties } from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import type { Message } from "./embed";
import { sdkActionManager } from "./sdk-event";
type Theme = "dark" | "light";
export type BookerLayouts = "month_view" | "week_view" | "column_view";
export type EmbedThemeConfig = Theme | "auto";
export type UiConfig = {
hideEventTypeDetails?: boolean;
// If theme not provided we would get null
theme?: EmbedThemeConfig | null;
styles?: EmbedStyles & EmbedNonStylesConfig;
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
layout?: BookerLayouts;
colorScheme?: string | null;
};
import type { EmbedThemeConfig, UiConfig, EmbedNonStylesConfig, BookerLayouts, EmbedStyles } from "./types";
type SetStyles = React.Dispatch<React.SetStateAction<EmbedStyles>>;
type setNonStylesConfig = React.Dispatch<React.SetStateAction<EmbedNonStylesConfig>>;
declare global {
interface Window {
CalEmbed: {
__logQueue?: unknown[];
embedStore: typeof embedStore;
applyCssVars: (cssVarsPerTheme: UiConfig["cssVarsPerTheme"]) => void;
};
}
}
/**
* This is in-memory persistence needed so that when user browses through the embed, the configurations from the instructions aren't lost.
*/
@@ -47,20 +40,6 @@ const embedStore = {
setUiConfig: [] as ((arg0: UiConfig) => void)[],
};
declare global {
interface Window {
CalEmbed: {
__logQueue?: unknown[];
embedStore: typeof embedStore;
applyCssVars: (cssVarsPerTheme: UiConfig["cssVarsPerTheme"]) => void;
};
CalComPageStatus: string;
isEmbed?: () => boolean;
getEmbedNamespace: () => string | null;
getEmbedTheme: () => EmbedThemeConfig | null;
}
}
let isSafariBrowser = false;
const isBrowser = typeof window !== "undefined";
@@ -100,23 +79,6 @@ function log(...args: unknown[]) {
}
}
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
// Keep this list to minimum, only adding those styles which are really needed.
interface EmbedStyles {
body?: Pick<CSSProperties, "background">;
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
availabilityDatePicker?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
}
interface EmbedNonStylesConfig {
/** Default would be center */
align?: "left";
branding?: {
brandColor?: string;
};
}
const setEmbedStyles = (stylesConfig: EmbedStyles) => {
embedStore.styles = stylesConfig;
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
+2 -7
View File
@@ -2,17 +2,12 @@
import { FloatingButton } from "./FloatingButton/FloatingButton";
import { Inline } from "./Inline/inline";
import { ModalBox } from "./ModalBox/ModalBox";
import type {
InterfaceWithParent,
interfaceWithParent,
UiConfig,
EmbedThemeConfig,
BookerLayouts,
} from "./embed-iframe";
import type { InterfaceWithParent, interfaceWithParent } from "./embed-iframe";
import css from "./embed.css";
import { SdkActionManager } from "./sdk-action-manager";
import type { EventData, EventDataMap } from "./sdk-action-manager";
import allCss from "./tailwind.generated.css?inline";
import type { UiConfig, EmbedThemeConfig, BookerLayouts } from "./types";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;
+45
View File
@@ -0,0 +1,45 @@
import type { CSSProperties } from "react";
type Theme = "dark" | "light";
export type EmbedThemeConfig = Theme | "auto";
export type BookerLayouts = "month_view" | "week_view" | "column_view";
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
// Keep this list to minimum, only adding those styles which are really needed.
export interface EmbedStyles {
body?: Pick<CSSProperties, "background">;
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
availabilityDatePicker?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
}
export interface EmbedNonStylesConfig {
/** Default would be center */
align?: "left";
branding?: {
brandColor?: string;
};
}
export type UiConfig = {
hideEventTypeDetails?: boolean;
// If theme not provided we would get null
theme?: EmbedThemeConfig | null;
styles?: EmbedStyles & EmbedNonStylesConfig;
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
layout?: BookerLayouts;
colorScheme?: string | null;
};
declare global {
interface Window {
CalComPageStatus: string;
isEmbed?: () => boolean;
getEmbedNamespace: () => string | null;
getEmbedTheme: () => EmbedThemeConfig | null;
}
}
export {};
@@ -14,9 +14,10 @@ const api = getCalApi();
test("Check that the API is available", async () => {
expect(api).toBeDefined()
const awaitedApi = await api;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
awaitedApi('floatingButton', {
calLink: 'free',
config: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error We are intentionaly testing invalid value
layout: 'wrongview'
}
@@ -72,7 +72,7 @@ export async function getServerSession(options: {
image: `${CAL_URL}/${user.username}/avatar.png`,
impersonatedByUID: token.impersonatedByUID ?? undefined,
belongsToActiveTeam: token.belongsToActiveTeam,
organizationId: token.organizationId,
org: token.org,
locale: user.locale ?? undefined,
},
};
@@ -8,6 +8,7 @@ import GoogleProvider from "next-auth/providers/google";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { getOrgFullDomain, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
@@ -17,7 +18,7 @@ import { isENVDev } from "@calcom/lib/env";
import { randomString } from "@calcom/lib/random";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { ErrorCode } from "./ErrorCode";
@@ -30,6 +31,8 @@ const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
const ORGANIZATIONS_AUTOLINK =
process.env.ORGANIZATIONS_AUTOLINK === "1" || process.env.ORGANIZATIONS_AUTOLINK === "true";
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
@@ -53,6 +56,33 @@ export const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T) =>
return metadata.success && metadata.data?.subscriptionId;
});
const checkIfUserShouldBelongToOrg = async (idP: IdentityProvider, email: string) => {
const [orgUsername, apexDomain] = email.split("@");
if (!ORGANIZATIONS_AUTOLINK || idP !== "GOOGLE") return { orgUsername, orgId: undefined };
const existingOrg = await prisma.team.findFirst({
where: {
AND: [
{
metadata: {
path: ["isOrganizationVerified"],
equals: true,
},
},
{
metadata: {
path: ["orgAutoAcceptEmail"],
equals: apexDomain,
},
},
],
},
select: {
id: true,
},
});
return { orgUsername, orgId: existingOrg?.id };
};
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
@@ -402,7 +432,14 @@ export const AUTH_OPTIONS: AuthOptions = {
username: true,
name: true,
email: true,
organizationId: true,
organization: {
select: {
id: true,
name: true,
slug: true,
metadata: true,
},
},
role: true,
locale: true,
teams: {
@@ -419,12 +456,23 @@ export const AUTH_OPTIONS: AuthOptions = {
// Check if the existingUser has any active teams
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser;
const { teams: _teams, organization, ...existingUserWithoutTeamsField } = existingUser;
const parsedOrgMetadata = teamMetadataSchema.parse(organization?.metadata ?? {});
return {
...existingUserWithoutTeamsField,
...token,
belongsToActiveTeam,
org: organization
? {
id: organization.id,
name: organization.name,
slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "",
fullDomain: getOrgFullDomain(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""),
domainSuffix: subdomainSuffix(),
}
: undefined,
};
};
if (!user) {
@@ -448,7 +496,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
organizationId: user?.organizationId,
org: user?.org,
locale: user?.locale,
};
}
@@ -487,7 +535,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
organizationId: token?.organizationId,
org: token?.org,
locale: existingUser.locale,
};
}
@@ -507,7 +555,7 @@ export const AUTH_OPTIONS: AuthOptions = {
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
organizationId: token?.organizationId,
org: token?.org,
locale: token.locale,
},
};
@@ -716,16 +764,26 @@ export const AUTH_OPTIONS: AuthOptions = {
return "/auth/error?error=use-identity-login";
}
// Associate with organization if enabled by flag and idP is Google (for now)
const { orgUsername, orgId } = await checkIfUserShouldBelongToOrg(idP, user.email);
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
username: orgId ? slugify(orgUsername) : usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: account.providerAccountId,
...(orgId && {
verified: true,
organization: { connect: { id: orgId } },
teams: {
create: { role: MembershipRole.MEMBER, accepted: true, team: { connect: { id: orgId } } },
},
}),
},
});
@@ -327,6 +327,7 @@ const BookerComponent = ({
prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
/>
</BookerSection>
</AnimatePresence>
@@ -19,6 +19,7 @@ type AvailableTimeSlotsProps = {
prefetchNextMonth: boolean;
monthCount: number | undefined;
seatsPerTimeSlot?: number | null;
showAvailableSeatsCount?: boolean | null;
};
/**
@@ -32,6 +33,7 @@ export const AvailableTimeSlots = ({
extraDays,
limitHeight,
seatsPerTimeSlot,
showAvailableSeatsCount,
prefetchNextMonth,
monthCount,
}: AvailableTimeSlotsProps) => {
@@ -60,6 +62,7 @@ export const AvailableTimeSlots = ({
seatsPerTimeSlot,
attendees,
bookingUid,
showAvailableSeatsCount,
});
if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) {
@@ -116,6 +119,7 @@ export const AvailableTimeSlots = ({
date={dayjs(slots.date)}
slots={slots.slots}
seatsPerTimeSlot={seatsPerTimeSlot}
showAvailableSeatsCount={showAvailableSeatsCount}
availableMonth={
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
? dayjs(slots.date).format("MMM")
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -130,13 +131,12 @@ export const EventMeta = () => {
<EventMetaBlock icon={User} className={`${colorClass}`}>
<div className="text-bookinghighlight flex items-start text-sm">
<p>
{bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "}
{eventTotalSeats}{" "}
{t("seats_available", {
count: bookingSeatAttendeesQty
? eventTotalSeats - bookingSeatAttendeesQty
: eventTotalSeats,
})}
<SeatsAvailabilityText
showExact={!!seatedEventData.showAvailableSeatsCount}
totalSeats={eventTotalSeats}
bookedSeats={bookingSeatAttendeesQty || 0}
variant="fraction"
/>
</p>
</div>
</EventMetaBlock>
@@ -33,6 +33,7 @@ type SeatedEventData = {
seatsPerTimeSlot?: number | null;
attendees?: number;
bookingUid?: string;
showAvailableSeatsCount?: boolean | null;
};
export type BookerStore = {
@@ -206,6 +207,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
seatsPerTimeSlot: undefined,
attendees: undefined,
bookingUid: undefined,
showAvailableSeatsCount: true,
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
@@ -12,6 +12,7 @@ import { Button, SkeletonText } from "@calcom/ui";
import { useBookerStore } from "../Booker/store";
import { useTimePreferences } from "../lib";
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
import { TimeFormatToggle } from "./TimeFormatToggle";
type AvailableTimesProps = {
@@ -24,6 +25,7 @@ type AvailableTimesProps = {
bookingUid?: string
) => void;
seatsPerTimeSlot?: number | null;
showAvailableSeatsCount?: boolean | null;
showTimeFormatToggle?: boolean;
className?: string;
availableMonth?: string | undefined;
@@ -35,6 +37,7 @@ export const AvailableTimes = ({
slots,
onTimeSelect,
seatsPerTimeSlot,
showAvailableSeatsCount,
showTimeFormatToggle = true,
className,
availableMonth,
@@ -110,15 +113,16 @@ export const AvailableTimes = ({
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
{hasTimeSlots && !bookingFull && (
<p className="flex items-center text-sm lowercase">
<p className="flex items-center text-sm">
<span
className={classNames(colorClass, "mr-1 inline-block h-2 w-2 rounded-full")}
aria-hidden
/>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "}
{t("seats_available", {
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
})}
<SeatsAvailabilityText
showExact={!!showAvailableSeatsCount}
totalSeats={seatsPerTimeSlot}
bookedSeats={slot.attendees || 0}
/>
</p>
)}
</Button>
@@ -0,0 +1,51 @@
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
type Props = {
/**
* Whether to show the exact number of seats available or not
*
* @default true
*/
showExact: boolean;
/**
* Shows available seats count as either whole number or fraction.
*
* Applies only when `showExact` is `true`
*
* @default "whole"
*/
variant?: "whole" | "fraction";
/** Number of seats booked in the event */
bookedSeats: number;
/** Total number of seats in the event */
totalSeats: number;
};
export const SeatsAvailabilityText = ({
showExact = true,
bookedSeats,
totalSeats,
variant = "whole",
}: Props) => {
const { t } = useLocale();
const availableSeats = totalSeats - bookedSeats;
const isHalfFull = bookedSeats / totalSeats >= 0.5;
const isNearlyFull = bookedSeats / totalSeats >= 0.83;
return (
<span className={classNames(showExact && "lowercase")}>
{showExact
? `${availableSeats}${variant === "fraction" ? ` / ${totalSeats}` : ""} ${t("seats_available", {
count: availableSeats,
})}`
: isNearlyFull
? t("seats_nearly_full")
: isHalfFull
? t("seats_half_full")
: t("seats_available", {
count: availableSeats,
})}
</span>
);
};
@@ -21,7 +21,10 @@ function RenderIcon({
return (
<img
src={eventLocationType.iconUrl}
className="me-[10px] h-4 w-4 opacity-70 invert-[.65] dark:invert-0"
className={classNames(
eventLocationType?.iconUrl?.includes("-dark") && "dark:invert",
"me-[10px] h-4 w-4"
)}
alt={`${eventLocationType.label} icon`}
/>
);
@@ -0,0 +1,55 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import { Title, CustomArgsTable } from "@calcom/storybook/components";
import { StorybookTrpcProvider } from "../../../ui/components/mocks/trpc.tsx";
import { VerifyCodeDialog } from "./VerifyCodeDialog";
<Meta title="Features/VerifyCodeDialog" component={VerifyCodeDialog} />
<Title title="VerifyCodeDialog" suffix="Brief" subtitle="Version 1.0 — Last Update: 11 Sep 2023" />
## Definition
The `VerifyCodeDialog` component allows users to enter a verification code sent to their email. The component provides feedback in case of an error and can handle different verification processes depending on whether the user session is required.
## Structure
This component contains an input form to capture a 6-digit verification code, error handling UI, and action buttons.
<CustomArgsTable of={VerifyCodeDialog} />
## VerifyCodeDialog Story
<Canvas>
<Story
name="VerifyCodeDialog"
args={{
isOpenDialog: true,
setIsOpenDialog: () => {},
email: "example@email.com",
onSuccess: () => {},
onError: (err) => {
if (err.message === "invalid_code") {
alert("Code provided is invalid");
}
},
}}
argTypes={{
isOpenDialog: { control: "boolean", description: "Indicates whether the dialog is open or not." },
setIsOpenDialog: { action: "setIsOpenDialog", description: "Function to set the dialog state." },
email: { control: "text", description: "Email to which the verification code was sent." },
onSuccess: { action: "onSuccess", description: "Callback function when verification succeeds." },
onError: { action: "onError", description: "Callback function when verification fails." },
isUserSessionRequiredToVerify: {
control: "boolean",
description: "Indicates if user session is required for verification.",
},
}}>
{(args) => (
<StorybookTrpcProvider>
<VerifyCodeDialog {...args} />
</StorybookTrpcProvider>
)}
</Story>
</Canvas>
@@ -26,7 +26,8 @@ import { handleRefundError } from "@calcom/lib/payment/handleRefundError";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
@@ -44,7 +45,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
user: {
select: {
id: true,
credentials: true, // Not leaking at the moment, be careful with
credentials: { select: credentialForCalendarServiceSelect }, // Not leaking at the moment, be careful with
email: true,
timeZone: true,
timeFormat: true,
@@ -434,6 +435,7 @@ async function handler(req: CustomRequest) {
where: {
id: credentialId,
},
select: credentialForCalendarServiceSelect,
});
if (foundCalendarCredential) {
calendarCredential = foundCalendarCredential;
@@ -725,6 +727,7 @@ async function handleSeatedEventCancellation(
where: {
id: reference.credentialId,
},
select: credentialForCalendarServiceSelect,
});
if (credential) {
@@ -733,13 +736,7 @@ async function handleSeatedEventCancellation(
attendees: evt.attendees.filter((evtAttendee) => attendee.email !== evtAttendee.email),
};
if (reference.type.includes("_video")) {
integrationsToUpdate.push(
updateMeeting(
{ ...credential, appName: evt.location?.replace("integrations:", "") || "" },
updatedEvt,
reference
)
);
integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference));
}
if (reference.type.includes("_calendar")) {
const calendar = await getCalendar(credential);
@@ -470,6 +470,128 @@ describe.sequential("handleNewBooking", () => {
},
timeout
);
test(
`should create a successful booking when location is provided as label of an option(Done for Organizer Address)
1. Should create a booking in the database
2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
});
mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "New York",
});
expectBookingToBeInDatabase({
description: "",
eventType: {
connect: {
id: mockBookingData.eventTypeId,
},
},
status: BookingStatus.ACCEPTED,
});
expectWorkflowToBeTriggered();
const testEmails = emails.get();
expect(testEmails[0]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
});
expect(testEmails[1]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
});
expect(testEmails[1].html).toContain("<title>confirmed_event_type_subject</title>");
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED",
payload: {
metadata: {
},
responses: {
name: { label: "your_name", value: "Booker" },
email: { label: "email_address", value: "booker@example.com" },
location: {
label: "location",
value: { optionValue: "", value: "New York" },
},
title: { label: "what_is_this_meeting_about" },
notes: { label: "additional_notes" },
guests: { label: "additional_guests" },
rescheduleReason: { label: "reason_for_reschedule" },
},
},
});
},
timeout
);
});
});

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