Compare commits

...
164 Commits
Author SHA1 Message Date
Alex van Andel a810a538e9 v3.2.5
release.yml / release (push) Canceled after 0s
2023-08-25 17:23:22 +01:00
sean-brydon 1cd0d7b5ca use team id :/ (#10957) 2023-08-25 16:14:28 +00:00
Alex van Andel 4540c79580 v3.2.4
release.yml / release (push) Canceled after 0s
2023-08-25 15:32:55 +01:00
Udit Takkar 00d01882d4 fix: private team regression (#10914)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-25 14:43:48 +01:00
Omar López 6ce4122528 fix: allows patching booking and period limits from API (#10935) 2023-08-25 14:29:04 +01:00
7ddbcfcdcf feat: Adds filter-by-email on /users/_get endpoint for admin calls (#10928)
* adds filter by email on users get for admins

* swagger update

* Update apps/api/pages/api/users/_get.ts

Co-authored-by: Omar López <zomars@me.com>

* .parse is required, also fixed some other issues

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-25 14:15:13 +01:00
b1b01ed383 remove close button from TopBanner (#10705)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-25 15:50:39 +03:00
6f5d9dc41f test: Create unit tests for react components in packages/ui/components/form/select (#10459)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 12:18:53 +00:00
GitStart-Cal.comandgitstart-calcom ac190b89dd chore: add UnpublishedEntity in storybook (CALCOM-10760) (#10864)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-25 15:12:19 +03:00
990441295c chore: add ImageUploader in storybook (CALCOM-10760) (#10865)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 15:06:55 +03:00
1e4c20073c chore: fix all @typescript-eslint/no-non-null-assertion warnings (#10635)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 14:56:20 +03:00
80353d67a0 chore: fix all @typescript-eslint/no-unused-vars warnings (#10645)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 11:48:24 +00:00
Keith Williams d55b4b051a perf: Fix slowest db query (again) (#10954) 2023-08-25 14:27:37 +03:00
Hariom Balhara 25d90e3e12 test: Upcoming bookings sorting order (#10950)
* Fix order by for bookings array
Fix orderBy for bookings

* Test

* Text change

* Add comments
2023-08-25 12:55:41 +03:00
Hariom Balhara 1727eb5625 fix: Unordered bookings (#10943) 2023-08-25 15:06:57 +05:30
sean-brydon 58f4224c66 Fix invite flow (#10951) 2023-08-25 09:19:03 +00:00
Udit Takkar 5d491e0a12 fix: unauthorized after email change (#10821) 2023-08-24 16:47:34 -07:00
alannncandOmar López 1eef5a56c3 fix: include orgs slug in getUserByUsername just like [user]/[type] page (#10723)
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:41:06 -07:00
d6740503be feat: Column View Available Slots Fix (#9815)
Co-authored-by: Vinoth Kumar V <vinoth_kumar_v@Vinoths-MacBook-Pro.local>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:39:45 -07:00
David Oksman 9f647ead46 fix: missing timezones in dropdown list (#10823) 2023-08-24 13:57:11 -07:00
Thibault Mathian f480afcace fix: caching and fetching of feature flags (#10837) 2023-08-24 13:43:52 -07:00
Hariom Balhara 8d73d15b8a perf: Lazy load framer motion stuff as much as we can (#10876) 2023-08-24 13:40:01 -07:00
Hariom Balhara e8370ef950 perf: Remove non booker apps from booker page (#10905) 2023-08-24 13:38:15 -07:00
Hariom Balhara e859c8dc58 test: Add attendee phone number test (#10932) 2023-08-24 11:21:49 -07:00
Keith Williams b4b38b9bc3 v3.2.3
release.yml / release (push) Canceled after 0s
2023-08-24 17:16:02 +03:00
Crowdin Bot d32719ad8b New Crowdin translations by Github Action 2023-08-24 12:47:12 +00:00
Hariom Balhara 99d0816d9f phoneSchema missing async parsing (#10929) 2023-08-24 12:43:52 +00:00
95f4b89fe8 fix: #10783 (UI updated on API Creation Form ) (#10786)
* Fix #10783

* Update the changes #10783

* Updated the changes on the another dialog footer

* Updated the changes

---------

Co-authored-by: Hem Pun <hemthegentlmen@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-24 11:20:48 +01:00
Abhijeet SinghandAbhijeet Singh 3e63c2b717 fix: Italics not being shown in the description after saving (#10848)
* added ignore rule for em tag in turndown

* added ignore rule for em tag in turndown

---------

Co-authored-by: Abhijeet Singh <asing9829@gmail.com>
2023-08-24 11:20:00 +01:00
Omar López 42cd56578d fix: Reduced boilerplate for publicViewer router (#10849) 2023-08-24 11:19:24 +01:00
Ujwal KumarandBailey Pumfleet 31d4cb4eb7 Transform # character to - in slug (#10896)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-24 11:19:10 +01:00
sean-brydonandBailey Pumfleet fde3e8bea9 Invalidate and add success message (#10907)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-24 11:18:59 +01:00
GitStart-Cal.comandgitstart-calcom 874ad29676 chore: add Table in storybook (CALCOM-10760) (#10869)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-24 11:07:57 +01:00
Hariom Balhara 5503d9d432 perf: bookings query improvement (#10687) 2023-08-24 11:14:10 +02:00
Keith WilliamsandShivam Kalra 43b3d68447 chore: Don't render certain components when running meticulous tests (#10772)
* chore: Don't render credits when running meticulous tests

* fix: type window Meticulous

* fix: window reference error

* chore: window interface

* chore: disable tzdialog popup on meticulous

* chore: IS_VISUAL_REGRESSION_TESTING

* fix: constants

* fix: only run on preview builds

---------

Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-24 13:42:53 +05:30
GitStart-Cal.comandgitstart-calcom e3c747a867 test: Create unit tests for react components is packages/ui/components/navigation (#10698)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-24 13:23:54 +05:30
sean-brydonandOmar López e020a2c9a4 feat: Availability Slider enabled for teams (#10902)
Co-authored-by: Omar López <zomars@me.com>
2023-08-23 21:23:24 +00:00
Leo Giovanetti e6e6f09547 feat: admin org list now shows all, dns check added (#10875)
* chore: admin org list now shows all, dns check added

* Updating email template

* type fixes
2023-08-23 21:01:12 +00:00
Leo GiovanettiandJoe Au-Yeung 68ec946c59 fix: add org avatar to org booking pages [CAL-2333] (#10847)
* Add org avatar to org booking pages

* Using web app url for org avatars + missing i18n string

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-23 20:10:02 +00:00
Omar López 5a6e5ce427 v3.2.2
release.yml / release (push) Canceled after 0s
2023-08-23 11:49:37 -07:00
Carina WollendorferandCarinaWolli b2382211db fix: adding a new workflow step that requires kyc verification (#10912)
* fix verificaiton check when step is added

* add verify badge with dialog also when adding action

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-23 20:32:14 +02:00
Joe Au-Yeung 12148506bd fix: Remove multiple durations for seated events (#10910)
* Remove multiple durations for seated events

* Clean up
2023-08-23 18:25:44 +00:00
MontoandSMLukwiya b5666fd4c3 fix: team availability shows only to the member owning it (#10738)
Co-authored-by: SMLukwiya <sundaymorganl@gmail.com>
2023-08-23 12:27:19 -04:00
Omar López bc7da563d8 fix: Prevents mixed server and client code (#10909) 2023-08-23 16:25:43 +00:00
d3acf4160d fix: data collection for headless router (#9183)
Co-authored-by: Wesley <100464352+ologbonowiwi@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-08-23 11:12:51 -04:00
Crowdin Bot 65e0c0e4c5 New Crowdin translations by Github Action 2023-08-23 12:16:51 +00:00
nicktrn 2dda6a509b chore: cron workflow tidying (#10127) 2023-08-23 17:43:22 +05:30
Crowdin Bot 7238414864 New Crowdin translations by Github Action 2023-08-23 09:11:55 +00:00
Shivam Kalra bab41ca8a6 chore: expect emails e2e (#10881)
* refactor: emails e2e

* fix: email subject with nameless organizer

* fix: comment on NEXT_PUBLIC_IS_E2E

* chore: E2E_TEST_MAILHOG_ENABLED env

* feat: IS_MAILHOG_ENABLED

* doc: E2E_TEST_MAILHOG_ENABLED

* fix: comment

* fix: env order

* chore: add E2E_TEST_MAILHOG_ENABLED to e2e env
2023-08-23 11:08:14 +02:00
Carina WollendorferandCarinaWolli 376f023e0f fix: scheduling workflow emails (#10890)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-08-22 12:29:06 -07:00
zomars 0cbf525a78 fix: Disables es-419 until translated 2023-08-22 12:01:50 -07:00
zomars f56fefec4d fix: Disables es-419 until translated 2023-08-22 12:00:20 -07:00
Crowdin Bot 66eccd6dd8 New Crowdin translations by Github Action 2023-08-22 18:12:25 +00:00
Omar López c4b9a9e2f4 fix: client locale inference (#10893)
* fix: client locale inference

* Update I18nLanguageHandler.tsx

* More fixes

* Update I18nLanguageHandler.tsx

* Update I18nLanguageHandler.tsx

* Addressing feedback

* Fix: first render issues

* Update Credits.tsx

* Fixed tests

* fix: try without loose locale pick

* Revert "fix: try without loose locale pick"

This reverts commit 5ed649c13117fee8bc507098353de0f18e98d971.

* fix: sorts locales so specifics are chosen first

* fix: switches client locale matcher
2023-08-22 20:08:34 +02:00
Crowdin Bot 38921e58cd New Crowdin translations by Github Action 2023-08-22 13:46:36 +00:00
Crowdin Bot 2c9c6c77f7 New Crowdin translations by Github Action 2023-08-22 13:43:30 +00:00
Syed Ali Shahbaz 46605e049d fix: Improves the Login button for SAML/OIDC (#10885) 2023-08-22 15:39:50 +02:00
Crowdin Bot 01e1e82ced New Crowdin translations by Github Action 2023-08-22 13:20:24 +00:00
Crowdin Bot 8e17716094 New Crowdin translations by Github Action 2023-08-22 13:17:20 +00:00
Crowdin Bot 7c028b6479 New Crowdin translations by Github Action 2023-08-22 13:14:18 +00:00
Crowdin Bot cba4bae5cd New Crowdin translations by Github Action 2023-08-22 13:10:46 +00:00
Omar López 55046f3017 chore: add constant for production builds (#10762) 2023-08-22 13:07:32 +00:00
Crowdin Bot 4836621c31 New Crowdin translations by Github Action 2023-08-22 12:37:46 +00:00
Omar López 6743aa4609 fix: client locale inference (#10850) 2023-08-22 14:34:55 +02:00
Crowdin Bot bbad0fbc18 New Crowdin translations by Github Action 2023-08-22 12:32:40 +00:00
Crowdin Bot 8f6812ecb2 New Crowdin translations by Github Action 2023-08-22 12:30:01 +00:00
Crowdin Bot 303b09f341 New Crowdin translations by Github Action 2023-08-22 12:27:07 +00:00
Crowdin Bot 1f3ee41eb4 New Crowdin translations by Github Action 2023-08-22 12:24:09 +00:00
Crowdin Bot 01c711a81e New Crowdin translations by Github Action 2023-08-22 12:21:37 +00:00
Aswath SandHariom Balhara 16bb5c3384 feat: Added more query params to successRedirect page (#8271)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-08-22 12:18:36 +00:00
Crowdin Bot c55d97368e New Crowdin translations by Github Action 2023-08-22 11:09:29 +00:00
Crowdin Bot ef193aca31 New Crowdin translations by Github Action 2023-08-22 11:06:46 +00:00
Nate GriftandUdit Takkar c36a6797a9 test: added team & user having same name test (#10868)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-22 13:03:00 +02:00
f393fd4ad4 fix: Resolved Typescript Error in PR #9598 created by @vijeet-shah. (#10871)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: vijeet-shah <93502358+vijeet-shah@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-22 13:02:40 +02:00
Crowdin Bot 8987b9bdbc New Crowdin translations by Github Action 2023-08-22 10:33:33 +00:00
Crowdin Bot 68bf426af0 New Crowdin translations by Github Action 2023-08-22 10:30:43 +00:00
Crowdin Bot 1125900013 New Crowdin translations by Github Action 2023-08-22 10:27:47 +00:00
Crowdin Bot d272e823c3 New Crowdin translations by Github Action 2023-08-22 10:24:43 +00:00
Crowdin Bot 3ea11d4f38 New Crowdin translations by Github Action 2023-08-22 10:21:55 +00:00
Crowdin Bot b4dc64594c New Crowdin translations by Github Action 2023-08-22 10:19:05 +00:00
Crowdin Bot 6a763b3720 New Crowdin translations by Github Action 2023-08-22 10:16:14 +00:00
Crowdin Bot 252ac1c704 New Crowdin translations by Github Action 2023-08-22 10:13:06 +00:00
Crowdin Bot c51e42a061 New Crowdin translations by Github Action 2023-08-22 10:10:14 +00:00
Crowdin Bot e3f9e477fe New Crowdin translations by Github Action 2023-08-22 10:07:12 +00:00
Crowdin Bot b265970b01 New Crowdin translations by Github Action 2023-08-22 10:03:58 +00:00
Crowdin Bot 7b7b2ffc9a New Crowdin translations by Github Action 2023-08-22 10:00:41 +00:00
Crowdin Bot d019062709 New Crowdin translations by Github Action 2023-08-22 09:58:00 +00:00
Crowdin Bot ad9d728314 New Crowdin translations by Github Action 2023-08-22 09:55:10 +00:00
Crowdin Bot b564fb9a2f New Crowdin translations by Github Action 2023-08-22 09:52:15 +00:00
Crowdin Bot 9c05f6839c New Crowdin translations by Github Action 2023-08-22 09:49:20 +00:00
Crowdin Bot 4dea38f06d New Crowdin translations by Github Action 2023-08-22 09:46:30 +00:00
Crowdin Bot c4fc222204 New Crowdin translations by Github Action 2023-08-22 09:43:37 +00:00
Crowdin Bot 00c3a2dd80 New Crowdin translations by Github Action 2023-08-22 09:40:44 +00:00
Crowdin Bot 46fa0088f2 New Crowdin translations by Github Action 2023-08-22 09:37:44 +00:00
Crowdin Bot a19fd9b889 New Crowdin translations by Github Action 2023-08-22 09:34:39 +00:00
Crowdin Bot d379c10510 New Crowdin translations by Github Action 2023-08-22 09:31:11 +00:00
GitStart-Cal.comandgitstart-calcom f3a58145ee chore: add datePicker in storybook (CALCOM-10760) (#10784)
* Update datepicker.stories.mdx

* Update datepicker.stories.mdx

* Requested changes

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-22 10:27:27 +01:00
Crowdin Bot e6125dc361 New Crowdin translations by Github Action 2023-08-21 23:15:41 +00:00
zomars b78f718e9e Update yarn.lock 2023-08-21 16:12:41 -07:00
zomars 53e87f7769 fix: Removes duplicate locale keys 2023-08-21 16:11:04 -07:00
Crowdin Bot 86eb40e0fd New Crowdin translations by Github Action 2023-08-21 17:15:42 +00:00
Hariom BalharaandUdit Takkar 51caa6834a fix: Make identifier conform to RHF field requirement (#10860)
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-21 13:11:47 -04:00
7a7a5b5285 fix: Removing payment fee on stripe (#10652)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-21 12:33:34 +02:00
f14ffb9e44 chore: add Dialog component in storybook (#10822)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-08-21 09:58:26 +00:00
Harry Dobrev 001b524005 fix: icon in emails is considered a tracking pixel (#10807) 2023-08-21 11:48:06 +02:00
GitStart-Cal.comandgitstart-calcom a8cf4fb309 chore: add Editor in storybook (CALCOM-10760) (#10820)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-21 11:45:30 +02:00
Joe Au-Yeung 155775556d perf: GCal reduce calendar payload (#10853) 2023-08-21 11:27:18 +02:00
d66095bf9c test: adding confirmed payment test cases on Stripe (#10243)
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-21 11:12:39 +02:00
0ddab30340 chore: fix all @typescript-eslint/no-explicit-any warnings (#10656)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-21 11:01:56 +02:00
Moon Patel 2a9e061575 fix: Next/previous buttons on week view move 8 instead of 7 days (#10742) 2023-08-21 11:01:27 +02:00
Crowdin Bot 351dce5cfd New Crowdin translations by Github Action 2023-08-21 08:31:02 +00:00
Koushik Sai 3255d9012c fix: missing border-subtle class (#10838) 2023-08-21 09:28:34 +01:00
Crowdin Bot 226dcb3fcf New Crowdin translations by Github Action 2023-08-19 20:07:17 +00:00
Jon@1599andLeo Giovanetti f80e9b2558 feat: add basecamp integration to cal.com (#9195)
* feat: installing the app works

* Update yarn.lock

* feat: /api/callback now gets user auth info from basecamp

* feat: updated basecamp logo

* feat: added project dropdown on event apps page

* feat: basecamp event creation and deletion working

* feat: basecamp event rescheduling now works

* refactor(CalendarService): basecamp CaldendarService code clean up

* refactor: code cleanup for basecamp app API

* feat: updated event summary text sent to basecamp

* chore: updated basecamp images and contact info

* fix: fixed typescript errors and added logic to refresh tokens on event settings

* refactor(CaldendarService): used refreshAccessToken from helpers.ts instead

* chore: updated basecamp description

* fix: fixed incorrect import

* fix: accidentally deleted props to toggle app for event

* chore: updated .env.appStore.example and added README for app

* Update .env.appStore.example

Co-authored-by: Leo Giovanetti <hello@leog.me>

* feat: added basecamp userAgent in env instead of hardcoded value

* feat: updated README to include how to set basecamp user agent env

* fix: removed unused import

* feat: used URLSearchParams to construct url params

* fix: fixed typescript errors

* chore: updated README to include an example on how to set basecamp user-agent

* feat: using TRPC instead of REST

* chore: removed old projects REST code

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-08-19 17:04:56 -03:00
Keith Williamsandzomars 3f273235a6 perf: Cache translations per language/per release (#10843)
Co-authored-by: zomars <zomars@me.com>
2023-08-18 17:46:17 -07:00
zomars e8554ed5c5 fix: key warning 2023-08-18 15:50:44 -07:00
zomars a03744008c chore: Update orgs README 2023-08-18 14:26:33 -07:00
Leo GiovanettiandOmar López fe8b13885c fix: org admin has team member privileges [CAL-2338] (#10809)
Co-authored-by: Omar López <zomars@me.com>
2023-08-18 14:10:17 -07:00
Leo Giovanettiandzomars 8995dcc82a fix: org username check [CAL-2352] (#10773)
Co-authored-by: zomars <zomars@me.com>
2023-08-18 14:03:55 -07:00
zomars e8e0d03265 chore: Adds basic README for orgs 2023-08-18 12:26:59 -07:00
Crowdin Bot 56031f0358 New Crowdin translations by Github Action 2023-08-18 18:19:11 +00:00
Joe Au-Yeung 1ef0f830d3 fix: Username suffix on production [CAL-2262] (#10825) 2023-08-18 11:15:58 -07:00
Hariom Balhara 59e2f6e4a7 perf: Booker-Bundle Size Improvement - Lazy load react-select, remove lodash and Dom Purify (#10814) 2023-08-18 11:13:21 -07:00
+15 67bb2ed798 feat: Availability slider for orgs (#10740)
* Init + get timezone + offset data agh

* Add 12/24h mode - style correctly

* User users timezone + working hours. Still some stuff to figure out

* Multiple working hours

* move calc to once per day

* Demo with two users and differnt timezones

* availabillity control tab via search params

* WIP hover overlay

* THIS WORKS ISH

* fix: multiple duration getSchedule calls [CAL-2336] (#10709)

Co-authored-by: Omar López <zomars@me.com>

* New Crowdin translations by Github Action

* fix: If the input type "Name" is selected, the label can't be changed from our default label "Your Name" (#10618)

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

* New Crowdin translations by Github Action

* test: Create unit tests for react components in packages/ui/components/form/step (#10442)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: element call app added (#10585)

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

* New Crowdin translations by Github Action

* New Crowdin translations by Github Action

* fix: e2e test for rescheduling overlapping time (#10721)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* feat: mailhog fixture (#10606)

* feat: mailhog fixture

* fix: nodemailer to dispatch emails with e2e env

* fix: remove space from email subject

* feat: fixture getFirstEventAsOwner

* feat: assert email subjects

* fix: and enable dynamic booking test (#10642)

* fix and enable dynamic booking test

* remove page pause

---------

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

* fix: Broken team events if a user with the same name exists (#10724)

* fix: Broken team events if a user with the same name exists

* Fix tests + fix usernameList optionality

* Try to list calendars, if not continue (#10720)

Co-authored-by: Omar López <zomars@me.com>

* v3.1.9

* link to org settings (#10718)

* feat: app paypal payment (#8797)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>

* fix: RTL issues on booking pages + email confirmation (#10526)

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

* fix: merge conflict

* Fixing org slug (#10538)

* fix: paypal build fixes

* Fix avatar for org in Shell top (#10712)

* feat: add range of dates for availability over-ride (#10462)

* feat: add range of dates for availability over-ride

* chore: changed range select to multiple select

---------

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

* fix: border issue for time slots (#10577)

Co-authored-by: Raghul D <v-raghuld@microsoft.com>

* style: Fix text wrapping issue in button (#10725)

Co-authored-by: Omar López <zomars@me.com>

* New Crowdin translations by Github Action

* fix: App Install Dropdown Sort Properly [CAL-2285] (#10672)

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

* fix: link escaping in booking page (#10360)

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

* chore: fix refund i18n message (#10731)

* chore: remove tailwind-scrollbar warning (#10523)

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

* New Crowdin translations by Github Action

* chore: Simplified date overrides (#10728)

* chore: Simplified date overrides

* Fixed a test that had a date override that wasn't at midnight utc

* Wrote test that showed a fixed Europe/Brussels

* Lint fix

* New Crowdin translations by Github Action

* Fix offset time + fetching correct dates

* Deal with awkward minute offsets

* remove store overhead

* Format H based on tz

* Cleanup store logic

* Cleanup

* Remove comments

* Remove comments

* Remove yarn.lock

* Dark mode & v-align text fixes

* Move ButtonGroup to the left to prevent chevron from jumping

* Shift based on timezone (non-hour) and have 15min granularity in hour display background color

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Zain Gulbaz <zaingulbaz8@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Suyash Srivastava <suyashsrivastava5053@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Pradumn Kumar <pradumn@tealfeed.com>
Co-authored-by: Raghul <123321540+Raghul18@users.noreply.github.com>
Co-authored-by: Raghul D <v-raghuld@microsoft.com>
Co-authored-by: ABDERRAHMANI IDRISSI HAMZA <97639117+idrissi-hamza@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Danila <daniil.demidovich@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-18 16:32:24 +00:00
Crowdin Bot ea4618a244 New Crowdin translations by Github Action 2023-08-18 15:39:49 +00:00
Keith Williams 201a9d9356 perf: Fix slowest db query (#10842) 2023-08-18 17:36:57 +02:00
Crowdin Bot 40b0d57024 New Crowdin translations by Github Action 2023-08-18 11:01:22 +00:00
761f66fc5a Create RTL tests for Meta component (#10478)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-18 16:28:30 +05:30
2403d77166 test: Create unit tests for react components in packages/ui/components/createButton (#10644)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-18 14:22:14 +05:30
Anik Dhabal Babu b44f4d66f9 fix text align (#10799) 2023-08-18 12:03:04 +05:30
Joe Au-Yeung d178962c37 Fix readonly check (#10824) 2023-08-18 06:30:59 +02:00
Crowdin Bot cf4d2b4ef5 New Crowdin translations by Github Action 2023-08-17 18:00:26 +00:00
Hariom Balhara 3e4139ac23 perf: Lazy load libphonenumber-js only when validation is needed. (#10812) 2023-08-17 10:57:30 -07:00
Crowdin Bot 9fb736f67a New Crowdin translations by Github Action 2023-08-17 16:57:12 +00:00
Alex van Andel afc738141f v3.2.1
release.yml / release (push) Canceled after 0s
2023-08-17 17:36:48 +01:00
Syed Ali Shahbaz efa195059e feat: Add scheduleId to POST and PATCH requests in event-type endpoint (#10816)
* adds scheduleId to PATCH

* adds scheduleID to POST

* updated docs
2023-08-17 15:42:29 +00:00
nicktrn 0f2b6bbe1a fix: add 2fa and email verification grace period (#10771) 2023-08-17 16:13:04 +02:00
Alex van Andelandsean-brydon 28b94a865f perf: Create a seperate component for dependencies of BookEventForm (#10808)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-08-17 16:12:33 +02:00
GitStart-Cal.comandgitstart-calcom f45803fcfa chore: add Steps in storybook (CALCOM-10760) (#10810)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-17 09:26:08 +01:00
alannncandJoe Au-Yeung 053da1ec32 fix: currency for event types (#10792)
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2023-08-16 14:58:28 -07:00
nicktrnandSyed Ali Shahbaz f0dcfc83c8 feat: add support for apple travel time (#10660)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
2023-08-16 13:32:04 -07:00
5eb3af54b3 test: add apple calendar testing (#10137)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-08-16 12:49:10 -07:00
Crowdin Bot 3552bbc6cf New Crowdin translations by Github Action 2023-08-16 19:46:15 +00:00
Koushik Sai 028bd77c91 feat: rank apps with most installed for a chosen category (#10756) 2023-08-16 12:43:37 -07:00
Crowdin Bot 65a1aa6100 New Crowdin translations by Github Action 2023-08-16 18:06:30 +00:00
Hariom Balhara a09401c203 perf: Booker bundle size optimization - Remove sanitize-html dependency on client (#10801) 2023-08-16 11:03:21 -07:00
sean-brydon 7c23773636 Line clamp event type bio (#10795) 2023-08-16 10:59:42 +02:00
Udit TakkarandShivam Kalra 56ba8c0a07 fix: sucess page time (#10743)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-16 13:35:31 +05:30
alannnc 5924bc0990 fix update profile input metadata (#10791) 2023-08-15 17:27:48 -07:00
+27 10ffd9bacd feat: Org user table - bulk actions (#10504)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@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: cherish2003 <saicherissh90@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: rkreddy99 <rreddy@e2clouds.com>
Co-authored-by: varun thummar <Varun>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Pradumn Kumar <47187878+Pradumn27@users.noreply.github.com>
Co-authored-by: Richard Poelderl <richard.poelderl@gmail.com>
Co-authored-by: mohammed hussam <hussamkhatib20@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: sydwardrae <94979838+sydwardrae@users.noreply.github.com>
Co-authored-by: Janakiram Yellapu <jyellapu@vmware.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: sajanlamsal <saznlamsal@gmail.com>
Co-authored-by: Cherish <88829894+cherish2003@users.noreply.github.com>
Co-authored-by: Danila <daniil.demidovich@gmail.com>
Co-authored-by: Neel Patel <29038590+N-NeelPatel@users.noreply.github.com>
Co-authored-by: Rama Krishna Reddy <49095575+rkreddy99@users.noreply.github.com>
Co-authored-by: Varun Thummar <110765105+VARUN949@users.noreply.github.com>
Co-authored-by: Bhargav <bhargavtenali@gmail.com>
Co-authored-by: Pratik Kumar <kpratik1929@gmail.com>
Co-authored-by: Ritesh Patil <riteshsp2000@gmail.com>
2023-08-15 14:07:38 -07:00
Leo Giovanetti 20c45b5d88 fix: vercel subdomain version (#10790)
* Standarizing org subdomain url

* fix: upgrading vercel subdomain version
2023-08-15 20:44:48 +00:00
3cab37ed8d feat: api return schedule (#10769)
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Omar López <zomars@me.com>
2023-08-15 19:18:25 +00:00
Crowdin Bot 98799b50f0 New Crowdin translations by Github Action 2023-08-15 19:02:03 +00:00
zomars 8071016848 chore: prettier 2023-08-15 11:59:16 -07:00
Crowdin Bot aa5e2ac9a5 New Crowdin translations by Github Action 2023-08-15 14:48:59 +00:00
Keith Williams 06857a4a69 perf: Remove duplicate call for user event types (#10770)
* perf: Remove duplicate call for user events

* Removed unneeded .map() call

* Remove yarn.lock change

* Renamed variable for clarity
2023-08-15 15:46:24 +01:00
Anshuman PandeyandOmar López b0d1720d92 feat: Allow / disallow SEO indexing on booking pages (#10566)
Co-authored-by: Omar López <zomars@me.com>
2023-08-15 00:44:09 +00:00
zomars e148ee7823 chore: Removes all traces of ".prisma/client" 2023-08-14 17:28:43 -07:00
Omar López bb9a039931 fix: paypal setup and key validation on server (#10761)
* fixes: paypal setup and key validation on server

* Renamed for legiblity

* Prefer early returns

* Update getEnabledAppsFromCredentials.ts

* Updated getEnabledAppsFromCredentials references

* Update getEnabledAppsFromCredentials.ts
2023-08-14 17:18:26 -07:00
GitStart-Cal.comandgitstart-calcom 053f93ea0a Extract to a separate test (#10699)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-15 00:57:35 +05:30
2a982c6b6d Fix lower text of new button availability refresh (#10701)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-14 14:31:09 -04:00
MehulandUdit Takkar 85490fef4d fix: 150min duration time for scheduling not possibe (#10715)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-14 14:11:34 -04:00
Crowdin Bot 57afd7a2b1 New Crowdin translations by Github Action 2023-08-14 15:05:57 +00:00
Hariom BalharaandJoe Au-Yeung 0f9c3571fe fix: Show messaging app in locations list (#10741)
* Get messaging app as well through query because a messaging app is also a location app

* Fix type error

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-14 15:02:29 +00:00
507 changed files with 11179 additions and 4746 deletions
+9
View File
@@ -1,6 +1,7 @@
# ********** INDEX **********
#
# - APP STORE
# - BASECAMP
# - DAILY.CO VIDEO
# - GOOGLE CALENDAR/MEET/LOGIN
# - HUBSPOT
@@ -20,6 +21,14 @@
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - BASECAMP
# Used to enable Basecamp integration with Cal.com
# @see https://github.com/calcom/cal.com#obtaining-basecamp-client-id-and-secret
BASECAMP3_CLIENT_ID=
BASECAMP3_CLIENT_SECRET=
BASECAMP3_USER_AGENT=
# - DAILY.CO VIDEO
# Enables Cal Video. to get your key
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
+12 -2
View File
@@ -126,8 +126,7 @@ TWILIO_WHATSAPP_PHONE_NUMBER=
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
# This is used so we can bypass emails in auth flows for E2E testing
# Set it to "1" if you need to run E2E tests locally
# Set it to "1" if you need to run E2E tests locally.
NEXT_PUBLIC_IS_E2E=
# Used for internal billing system
@@ -172,6 +171,12 @@ EMAIL_SERVER_PORT=1025
## You will need to provision an App Password.
## @see https://support.google.com/accounts/answer/185833
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
# Used for E2E for email testing
# Set it to "1" if you need to email checks in E2E tests locally
# Make sure to run mailhog container manually or with `yarn dx`
E2E_TEST_MAILHOG_ENABLED=
# **********************************************************************************************************
# Set the following value to true if you wish to enable Team Impersonation
@@ -215,3 +220,8 @@ PROJECT_ID_VERCEL=
TEAM_ID_VERCEL=
# Get it from: https://vercel.com/account/tokens
AUTH_BEARER_TOKEN_VERCEL=
# - APPLE CALENDAR
# Used for E2E tests on Apple Calendar
E2E_TEST_APPLE_CALENDAR_EMAIL=""
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
@@ -5,16 +5,15 @@ on:
types:
- opened
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
contents: none
issues: read
pull-requests: write
steps:
- name: Apply labels from linked issue to PR
uses: actions/github-script@v5
@@ -1,7 +1,7 @@
# .github/workflows/chromatic.yml
# Workflow name
name: 'Chromatic'
name: "Chromatic"
# Event for the workflow
on:
+3 -3
View File
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs "At every 15th minute." (see https://crontab.guru)
- cron: "*/15 * * * *"
jobs:
cron-bookingReminder:
env:
@@ -20,4 +20,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
+2 -2
View File
@@ -5,7 +5,7 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “Every month at 1st (see https://crontab.guru)
# Runs "At 00:00 on day-of-month 1." (see https://crontab.guru)
- cron: "0 0 1 * *"
jobs:
cron-downgradeUsers:
@@ -21,4 +21,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs "At every 15th minute." (see https://crontab.guru)
- cron: "*/15 * * * *"
jobs:
cron-scheduleEmailReminders:
env:
@@ -20,4 +20,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs "At every 15th minute." (see https://crontab.guru)
- cron: "*/15 * * * *"
jobs:
cron-scheduleSMSReminders:
env:
@@ -20,4 +20,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
@@ -4,8 +4,8 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
# Runs "At every 15th minute." (see https://crontab.guru)
- cron: "*/15 * * * *"
jobs:
cron-scheduleWhatsappReminders:
env:
@@ -20,4 +20,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
+1 -1
View File
@@ -8,7 +8,7 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs every day (see https://crontab.guru)
# Runs "At 00:00." every day (see https://crontab.guru)
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
+2 -2
View File
@@ -5,7 +5,7 @@ on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “Every month at 1st (see https://crontab.guru)
# Runs "At 00:00 on day-of-month 1." (see https://crontab.guru)
- cron: "0 0 1 * *"
jobs:
cron-syncAppMeta:
@@ -21,4 +21,4 @@ jobs:
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
-sSf
+3
View File
@@ -41,6 +41,9 @@ jobs:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
+2
View File
@@ -38,6 +38,8 @@ jobs:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
+3
View File
@@ -41,6 +41,9 @@ jobs:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
+4 -1
View File
@@ -40,11 +40,14 @@ jobs:
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
@@ -7,6 +7,8 @@ env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
+2
View File
@@ -7,6 +7,8 @@ env:
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
+2 -2
View File
@@ -8,7 +8,7 @@ on: # yamllint disable-line rule:truthy
workflow_dispatch:
inputs:
RELEASE_TAG:
description: 'v{Major}.{Minor}.{Patch}'
description: "v{Major}.{Minor}.{Patch}"
jobs:
release:
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v3
- name: "Determine tag"
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
- name: "Run remote release workflow"
uses: "actions/github-script@v6"
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
@@ -41,7 +41,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
with:
header: pr-title-lint-error
message: |
Thank you for following the naming conventions! 🙏
Thank you for following the naming conventions! 🙏
+1
View File
@@ -1,6 +1,7 @@
name: Submodule Sync
on:
schedule:
# Runs "At minute 15 past every 4th hour." (see https://crontab.guru)
- cron: "15 */4 * * *"
workflow_dispatch: ~
jobs:
+1 -1
View File
@@ -40,4 +40,4 @@ vscode:
- bradlc.vscode-tailwindcss
- ban.spellright
- stripe.vscode-stripe
- Prisma.prisma
- Prisma.prisma
+13
View File
@@ -221,6 +221,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
```sh
docker pull mailhog/mailhog
@@ -461,6 +462,18 @@ following
4. Now paste the API key to your `.env` file into the `DAILY_API_KEY` field in your `.env` file.
5. If you have the [Daily Scale Plan](https://daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
### Obtaining Basecamp Client ID and Secret
1. Visit the [37 Signals Integrations Dashboard](launchpad.37signals.com/integrations) and sign in.
2. Register a new application by clicking the Register one now link.
3. Fill in your company details.
4. Select Basecamp 4 as the product to integrate with.
5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs.
6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields.
7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`.
For example, `Cal.com (support@cal.com)`.
### Obtaining HubSpot Client ID and Secret
1. Open [HubSpot Developer](https://developer.hubspot.com/) and sign into your account, or create a new one.
+10 -1
View File
@@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
position: true,
eventName: true,
timeZone: true,
schedulingType: true,
// START Limit future bookings
periodType: true,
periodStartDate: true,
schedulingType: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
// END Limit future bookings
requiresConfirmation: true,
disableGuests: true,
hideCalendarNotes: true,
@@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
slotInterval: true,
successRedirectUrl: true,
locations: true,
bookingLimits: true,
durationLimits: true,
})
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.partial()
@@ -67,6 +71,7 @@ const schemaEventTypeCreateParams = z
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
.strict();
@@ -85,6 +90,7 @@ const schemaEventTypeEditParams = z
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
.strict();
@@ -98,6 +104,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
position: true,
userId: true,
teamId: true,
scheduleId: true,
eventName: true,
timeZone: true,
periodType: true,
@@ -123,6 +130,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
seatsPerTimeSlot: true,
seatsShowAttendees: true,
bookingFields: true,
bookingLimits: true,
durationLimits: true,
}).merge(
z.object({
locations: z
@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
});
export const withValidQueryUserEmail = withValidation({
schema: schemaQueryUserEmail,
type: "Zod",
mode: "query",
});
+1 -1
View File
@@ -29,7 +29,7 @@ enum locales {
RO = "ro",
NL = "nl",
PT_BR = "pt-BR",
ES_419 = "es-419",
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
KO = "ko",
JA = "ja",
PL = "pl",
+39 -3
View File
@@ -1,9 +1,11 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
import getCalLink from "../_utils/getCalLink";
@@ -42,7 +44,8 @@ import getCalLink from "../_utils/getCalLink";
export async function getHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const event_type = await prisma.eventType.findUnique({
const eventType = await prisma.eventType.findUnique({
where: { id },
include: {
customInputs: true,
@@ -51,10 +54,43 @@ export async function getHandler(req: NextApiRequest) {
owner: { select: { username: true, id: true } },
},
});
await checkPermissions(req, eventType);
const link = event_type ? getCalLink(event_type) : null;
const link = eventType ? getCalLink(eventType) : null;
// user.defaultScheduleId doesn't work the same for team events.
if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) {
const user = await prisma.user.findUniqueOrThrow({
where: {
id: eventType.userId,
},
select: {
defaultScheduleId: true,
},
});
eventType.scheduleId = user.defaultScheduleId;
}
return { event_type: schemaEventTypeReadPublic.parse({ ...event_type, link }) };
// TODO: eventType when not found should be a 404
// but API consumers may depend on the {} behaviour.
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
}
type BaseEventTypeCheckPermissions = {
userId: number | null;
teamId: number | null;
};
async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
req: NextApiRequest,
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
) {
if (req.isAdmin) return true;
if (eventType?.teamId) {
req.query.teamId = String(eventType.teamId);
await canAccessTeamEventOrThrow(req, "MEMBER");
}
if (eventType?.userId === req.userId) return true; // is owner.
throw new HttpError({ statusCode: 403, message: "Forbidden" });
}
export default defaultResponder(getHandler);
+12 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import type { z } from "zod";
@@ -52,6 +52,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
* slug:
* type: string
* description: Unique slug for the event type
* scheduleId:
* type: number
* description: The ID of the schedule for this event type
* hosts:
* type: array
* items:
@@ -199,10 +202,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body);
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeEditBodyParams.parse(body);
const data: Prisma.EventTypeUpdateArgs["data"] = {
...parsedBody,
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
if (hosts) {
+71 -18
View File
@@ -1,8 +1,8 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import type { PrismaClient } from "@calcom/prisma/client";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
@@ -35,20 +35,12 @@ import getCalLink from "./_utils/getCalLink";
* description: No event types were found
*/
async function getHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const args: Prisma.EventTypeFindManyArgs = {
where: { userId },
};
/** Only admins can query other users */
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required" });
if (isAdmin && req.query.userId) {
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
args.where = { userId: { in: userIds } };
}
const { userId, prisma } = req;
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
const data = await prisma.eventType.findMany({
...args,
where: {
userId: { in: userIds },
},
include: {
customInputs: true,
team: { select: { slug: true } },
@@ -56,13 +48,74 @@ async function getHandler(req: NextApiRequest) {
owner: { select: { username: true, id: true } },
},
});
// this really should return [], but backwards compatibility..
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
return {
event_types: data.map((eventType) => {
const link = getCalLink(eventType);
return schemaEventTypeReadPublic.parse({ ...eventType, link });
}),
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
(eventType) => {
const link = getCalLink(eventType);
return schemaEventTypeReadPublic.parse({ ...eventType, link });
}
),
};
}
// TODO: Extract & reuse.
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
/** Guard: Only admins can query other users */
if (!isAdmin) {
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
}
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
}
type DefaultScheduleIdEventTypeBase = {
scheduleId: number | null;
userId: number | null;
};
// If an eventType is given w/o a scheduleId
// Then we associate the default user schedule id to the eventType
async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
prisma,
eventTypes,
userIds,
}: {
prisma: PrismaClient;
eventTypes: (T & Partial<Omit<T, keyof DefaultScheduleIdEventTypeBase>>)[];
userIds: number[];
}) {
// there is no event types without a scheduleId, skip the user query
if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes;
const users = await prisma.user.findMany({
where: {
id: {
in: userIds,
},
},
select: {
id: true,
defaultScheduleId: true,
},
});
if (!users.length) {
return eventTypes;
}
const defaultScheduleIds = users.reduce((result, user) => {
result[user.id] = user.defaultScheduleId;
return result;
}, {} as { [x: number]: number | null });
return eventTypes.map((eventType) => {
// realistically never happens, userId should't be null on personal event types.
if (!eventType.userId) return eventType;
return {
...eventType,
scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId],
};
});
}
export default defaultResponder(getHandler);
+13 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
@@ -60,6 +60,9 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* hidden:
* type: boolean
* description: If the event type should be hidden from your public booking page
* scheduleId:
* type: number
* description: The ID of the schedule for this event type
* position:
* type: integer
* description: The position of the event type on the public booking page
@@ -178,6 +181,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* position: 0
* eventName: null
* timeZone: null
* scheduleId: 5
* periodType: UNLIMITED
* periodStartDate: 2023-02-15T08:46:16.000Z
* periodEndDate: 2023-0-15T08:46:16.000Z
@@ -255,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma, body } = req;
const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {});
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeCreateBodyParams.parse(body || {});
let data: Prisma.EventTypeCreateArgs["data"] = {
...parsedBody,
userId,
users: { connect: { id: userId } },
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
await checkPermissions(req);
@@ -59,7 +59,7 @@ async function getHandler(req: NextApiRequest) {
};
const data = await prisma.eventType.findMany(args);
return { event_types: data.map((attendee) => schemaEventTypeReadPublic.parse(attendee)) };
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
}
export default defaultResponder(getHandler);
+20
View File
@@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
import { schemaUsersReadPublic } from "~/lib/validations/user";
/**
@@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
* schema:
* type: string
* description: Your API key
* - in: query
* name: email
* required: false
* schema:
* type: array
* items:
* type: string
* format: email
* style: form
* explode: true
* description: The email address or an array of email addresses to filter by
* tags:
* - users
* responses:
@@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
const where: Prisma.UserWhereInput = {};
// If user is not ADMIN, return only his data.
if (!isAdmin) where.id = userId;
if (req.query.email) {
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
where.email = {
in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email],
};
}
const [total, data] = await prisma.$transaction([
prisma.user.count({ where }),
prisma.user.findMany({ where, take, skip }),
+3 -3
View File
@@ -1,5 +1,5 @@
# Set the version of docker compose to use
version: '3.9'
version: "3.9"
# The containers that compose the project
services:
@@ -8,8 +8,8 @@ services:
restart: always
container_name: integration-tests-prisma
ports:
- '5433:5432'
- "5433:5432"
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests
POSTGRES_DB: tests
+21 -1
View File
@@ -1,9 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const ns = ["common"];
const supportedLngs = ["en", "fr"];
const resources = ns.reduce((acc, n) => {
supportedLngs.forEach((lng) => {
if (!acc[lng]) acc[lng] = {};
acc[lng] = {
...acc[lng],
[n]: require(`../../web/public/static/locales/${lng}/${n}.json`),
};
});
return acc;
}, {});
i18n.use(initReactI18next).init({
resources: [],
debug: true,
fallbackLng: "en",
defaultNS: "common",
ns,
interpolation: {
escapeValue: false,
},
react: { useSuspense: true },
resources,
});
export default i18n;
+1
View File
@@ -15,6 +15,7 @@ module.exports = {
"storybook-addon-rtl-direction",
"storybook-react-i18next",
"storybook-addon-next",
"storybook-addon-next-router",
/*{
name: "storybook-addon-next",
options: {
+4 -4
View File
@@ -1,9 +1,9 @@
<script>
window.global = window;
window.isEmbed = ()=> {
return location.search.includes("embed=")
}
window.isEmbed = () => {
return location.search.includes("embed=");
};
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap");
</style>
+16
View File
@@ -1,4 +1,5 @@
import { addDecorator } from "@storybook/react";
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
import { I18nextProvider } from "react-i18next";
import "../styles/globals.css";
@@ -13,6 +14,21 @@ export const parameters = {
date: /Date$/,
},
},
nextRouter: {
pathname: "/",
asPath: "/",
query: {},
push() {},
Provider: AppRouterContext.Provider,
},
globals: {
locale: "en",
locales: {
en: "English",
fr: "Français",
},
},
i18n,
};
addDecorator((storyFn) => (
+2 -1
View File
@@ -18,6 +18,7 @@ export function VariantsTable({
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
return (
<div
id="light-variant"
className={classNames(
isDark &&
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
@@ -43,7 +44,7 @@ export function VariantsTable({
</table>
</div>
{!isDark && (
<div data-mode="dark" className="dark">
<div id="dark-variant" data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children}
</VariantsTable>
+16 -6
View File
@@ -1,11 +1,21 @@
import { Meta } from '@storybook/addon-docs';
import { Meta } from "@storybook/addon-docs";
<Meta title="Introduction" />
<div className="text-center flex flex-col items-center">
<h1 style={{marginBottom: '24px', marginTop: '36px'}}>Welcome to Cal.com UI</h1>
<p>This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
<p>Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a> library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to the design team.</p>
<div className="flex flex-col items-center text-center">
<h1 style={{ marginBottom: "24px", marginTop: "36px" }}>Welcome to Cal.com UI</h1>
<p>
This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma
library, we will be adding more components as we go along.
</p>
<p>
Our{" "}
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
Figma
</a>{" "}
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
the design team.
</p>
</div>
<img src="/sb-cover.jpg" />
<img src="/sb-cover.jpg" />
+1
View File
@@ -23,6 +23,7 @@
"next": "^13.4.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook-addon-next-router": "^4.0.2",
"storybook-addon-rtl-direction": "^0.0.19"
},
"devDependencies": {
+53 -55
View File
@@ -1,8 +1,8 @@
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
.sbdocs {
font-family: 'Inter var' !important;
padding: 0!important;
font-family: "Inter var" !important;
padding: 0 !important;
}
#docs-root {
@@ -21,8 +21,8 @@
font-weight: 500;
border: none;
border-top: 2px solid rgba(0, 0, 0, 0.12);
padding-top: 12px!important;
padding-bottom: 12px!important;
padding-top: 12px !important;
padding-bottom: 12px !important;
margin: 82px 0 0 0;
}
@@ -37,7 +37,7 @@
}
/** Docs table **/
.custom-args-wrapper{
.custom-args-wrapper {
max-height: 400px;
overflow-y: scroll;
overflow-x: hidden;
@@ -45,7 +45,7 @@
}
.docblock-argstable-body {
box-shadow: none!important;
box-shadow: none !important;
font-size: 14px;
}
@@ -64,7 +64,7 @@
.docblock-argstable-body div p,
.docblock-argstable-body div span {
color: #8F8F8F !important;
color: #8f8f8f !important;
}
/** Custom components **/
@@ -83,8 +83,8 @@
}
.story-title h1 span {
color: #9CA3AF;
font-family: 'Inter var';
color: #9ca3af;
font-family: "Inter var";
font-weight: normal;
display: inline-block;
margin-left: 8px;
@@ -96,7 +96,7 @@
}
.examples {
background-color: #F9FAFB;
background-color: #f9fafb;
padding: 20px;
display: flex;
flex-direction: column;
@@ -111,14 +111,14 @@
}
.examples-title {
color: #8F8F8F;
color: #8f8f8f;
font-size: 14px;
margin-bottom: 30px;
}
.examples-item-title {
font-size: 12px;
color: #8F8F8F;
color: #8f8f8f;
margin-bottom: 12px;
display: block;
}
@@ -130,7 +130,7 @@
.examples-footnote p,
.examples-footnote ul,
.examples-footnote li {
color: #8F8F8F;
color: #8f8f8f;
font-size: 14px;
}
@@ -139,11 +139,11 @@
}
.examples-footnote li {
margin: 0!important;
margin: 0 !important;
}
.story-note {
background-color: #F9FAFB;
background-color: #f9fafb;
font-size: 14px;
padding: 20px;
margin-bottom: 12px;
@@ -161,12 +161,12 @@
box-shadow: none !important;
margin: 0 !important;
border: none !important;
border-radius: none!important;
border-radius: none !important;
}
.docs-story > div:first-child {
padding: 0!important;
margin: 0!important;
padding: 0 !important;
margin: 0 !important;
}
.docs-story .innerZoomElementWrapper > div {
@@ -174,7 +174,7 @@
}
.sb-main-padded {
padding: 0!important;
padding: 0 !important;
}
@media screen and (max-width: 1200px) {
@@ -198,74 +198,72 @@
:root {
/* background */
--cal-bg-emphasis: #E5E7EB;
--cal-bg-emphasis: #e5e7eb;
--cal-bg: white;
--cal-bg-subtle: #F3F4F6;
--cal-bg-muted: #F9FAFB;
--cal-bg-subtle: #f3f4f6;
--cal-bg-muted: #f9fafb;
--cal-bg-inverted: #111827;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
--cal-bg-info: #dee9fc;
--cal-bg-success: #e2fbe8;
--cal-bg-attention: #fceed8;
--cal-bg-error: #f9e3e2;
--cal-bg-dark-error: #752522;
/* Borders */
--cal-border-emphasis:#9CA3AF;
--cal-border: #D1D5DB;
--cal-border-subtle:#E5E7EB;
--cal-border-muted:#F3F4F6;
--cal-border-emphasis: #9ca3af;
--cal-border: #d1d5db;
--cal-border-subtle: #e5e7eb;
--cal-border-muted: #f3f4f6;
--cal-border-error: #aa2e26;
/* Content/Text */
--cal-text-emphasis: #111827;
--cal-text:#374151;
--cal-text-subtle:#6B7280;
--cal-text-muted:#9CA3AF;
--cal-text-inverted:white;
--cal-text: #374151;
--cal-text-subtle: #6b7280;
--cal-text-muted: #9ca3af;
--cal-text-inverted: white;
/* Content/Text -> components */
--cal-text-info:#253985;
--cal-text-success:#285231;
--cal-text-attention:#73321B;
--cal-text-error:#752522;
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321b;
--cal-text-error: #752522;
/* Brand shinanigans
-> These will be computed for the users theme at runtime.
*/
--cal-brand:#111827;
--cal-brand-emphasis:#101010;
--cal-brand: #111827;
--cal-brand-emphasis: #101010;
--cal-brand-text: white;
}
.dark {
/* background */
--cal-bg-emphasis: #2B2B2B;
--cal-bg-emphasis: #2b2b2b;
--cal-bg: #101010;
--cal-bg-subtle: #2B2B2B;
--cal-bg-muted: #1C1C1C;
--cal-bg-inverted: #F3F4F6;
--cal-bg-subtle: #2b2b2b;
--cal-bg-muted: #1c1c1c;
--cal-bg-inverted: #f3f4f6;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
--cal-bg-info: #dee9fc;
--cal-bg-success: #e2fbe8;
--cal-bg-attention: #fceed8;
--cal-bg-error: #f9e3e2;
--cal-bg-dark-error: #752522;
/* Borders */
--cal-border-emphasis: #575757;
--cal-border: #444444;
--cal-border-subtle: #2B2B2B;
--cal-border-muted: #1C1C1C;
--cal-border-subtle: #2b2b2b;
--cal-border-muted: #1c1c1c;
--cal-border-error: #aa2e26;
/* Content/Text */
--cal-text-emphasis: #F3F4F6;
--cal-text: #D6D6D6;
--cal-text-emphasis: #f3f4f6;
--cal-text: #d6d6d6;
--cal-text-subtle: #757575;
--cal-text-muted: #575757;
--cal-text-inverted: #101010;
@@ -273,7 +271,7 @@
/* Content/Text -> components */
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321B;
--cal-text-attention: #73321b;
--cal-text-error: #752522;
/* Brand shenanigans
+13 -12
View File
@@ -2,8 +2,8 @@ html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
Droid Sans, Helvetica Neue, sans-serif;
}
a {
@@ -21,7 +21,7 @@ a {
}
.swagger-ui .opblock .opblock-summary {
display: grid;
flex-direction: column;
flex-direction: column;
}
.opblock-summary-path {
flex-shrink: 0;
@@ -43,22 +43,22 @@ a {
font-size: 22px;
}
.swagger-ui .scheme-container {
padding: 14px 0;
padding: 14px 0;
}
.swagger-ui .info {
margin: 10px 0;
margin: 10px 0;
}
.swagger-ui .auth-wrapper {
margin: 10px 0;
}
.swagger-ui .authorization__btn {
display: none;
display: none;
}
.swagger-ui .opblock {
margin: 0 0 5px;
}
button.opblock-summary-control > svg {
display: none;
display: none;
}
.swagger-ui .filter .operation-filter-input {
border: 2px solid #d8dde7;
@@ -73,15 +73,16 @@ a {
.swagger-ui .info .title small {
top: 5px;
}
.swagger-ui a.nostyle, .swagger-ui a.nostyle:visited {
.swagger-ui a.nostyle,
.swagger-ui a.nostyle:visited {
width: 100%;
}
div.request-snippets > div.curl-command > div:nth-child(1) {
overscroll-behavior: contain;
overflow-x: scroll;
}
.swagger-ui .opblock-body pre.microlight {
font-size: 9px;
.swagger-ui .opblock-body pre.microlight {
font-size: 9px;
}
.swagger-ui table tbody tr td {
padding: 0px 0 0;
@@ -91,7 +92,7 @@ a {
font-size: 12px;
}
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > tbody > tr {
display: flex;
display: flex;
width: 100vw;
flex-direction: column;
font-size: 60%;
@@ -99,4 +100,4 @@ a {
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > thead > tr {
display: none;
}
}
}
+37 -13
View File
@@ -1,27 +1,51 @@
import { lookup } from "bcp-47-match";
import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
export function useViewerI18n() {
return trpc.viewer.public.i18n.useQuery(undefined, {
staleTime: Infinity,
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
trpc: {
context: { skipBatch: true },
},
});
function useViewerI18n(locale: string) {
return trpc.viewer.public.i18n.useQuery(
{ locale, CalComVersion: CALCOM_VERSION },
{
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
**/
trpc: {
context: { skipBatch: true },
},
}
);
}
function useClientLocale(locales: string[]) {
const session = useSession();
// If the user is logged in, use their locale
if (session.data?.user.locale) return session.data.user.locale;
// If the user is not logged in, use the browser locale
if (typeof window !== "undefined") {
// This is the only way I found to ensure the prefetched locale is used on first render
// FIXME: Find a better way to pick the best matching locale from the browser
return lookup(locales, window.navigator.language) || window.navigator.language;
}
// If the browser is not available, use English
return "en";
}
export function useClientViewerI18n(locales: string[]) {
const clientLocale = useClientLocale(locales);
return useViewerI18n(clientLocale);
}
/**
* Auto-switches locale client-side to the logged in user's preference
*/
const I18nLanguageHandler = () => {
const I18nLanguageHandler = (props: { locales: string[] }) => {
const { locales } = props;
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale || i18n.language;
const locale = useClientViewerI18n(locales).data?.locale || i18n.language;
useEffect(() => {
// bail early when i18n = {}
+2 -2
View File
@@ -6,7 +6,7 @@ import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { WEBAPP_URL, IS_CALCOM } from "@calcom/lib/constants";
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import type { AppProps } from "@lib/app-providers";
@@ -72,7 +72,7 @@ function PageWrapper(props: AppProps) {
}
{...seoConfig.defaultNextSeo}
/>
<I18nLanguageHandler />
<I18nLanguageHandler locales={props.router.locales || []} />
<Script
nonce={nonce}
id="page-status"
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@@ -368,7 +368,7 @@ function BookingListItem(booking: BookingItemProps) {
{t("error_collecting_card")}
</Badge>
) : booking.paid ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
<Badge className="ltr:mr-2 rtl:ml-2" variant="green" data-testid="paid_badge">
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
) : null}
@@ -35,6 +35,7 @@ import {
Tooltip,
} from "@calcom/ui";
import { Copy, Edit } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -286,36 +287,38 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
labelSrOnly
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
{!IS_VISUAL_REGRESSION_TESTING && (
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
labelSrOnly
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
)}
</div>
</SettingsToggle>
<hr className="border-subtle" />
@@ -338,6 +341,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
@@ -138,7 +138,7 @@ const EventTypeScheduleDetails = memo(
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
@@ -202,6 +202,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
isManaged: false,
});
}
// We push the selected schedule from the event type if it's not part of the list response. This happens if the user is an admin but not the schedule owner.
else if (eventType.schedule && !schedules.find((schedule) => schedule.id === eventType.schedule)) {
options.push({
value: eventType.schedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
setOptions(options);
@@ -12,6 +12,7 @@ import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import cx from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -116,6 +117,8 @@ export const EventSetupTab = (
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const locationOptions = props.locationOptions.map((locationOption) => {
const options = locationOption.options.filter((option) => {
@@ -129,10 +132,12 @@ export const EventSetupTab = (
};
});
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
}));
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180].map(
(mins) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
})
);
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
MultiValue<{
@@ -383,6 +388,9 @@ export const EventSetupTab = (
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
const urlPrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
return (
<div>
@@ -411,10 +419,10 @@ export const EventSetupTab = (
defaultValue={eventType.slug}
addOnLeading={
<>
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{urlPrefix}/
{!isManagedEventType
? team
? "team/" + team.slug
? (orgBranding ? "" : "team/") + team.slug
: eventType.users[0].username
: t("username_placeholder")}
/
@@ -501,6 +509,8 @@ export const EventSetupTab = (
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
disabled={seatsEnabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_multiple_durations") : undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
@@ -20,10 +20,11 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const { t } = useLocale();
const { nextStep } = props;
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId! },
{ scheduleId: defaultScheduleId ?? undefined },
{
enabled: !!defaultScheduleId,
enabled: !!scheduleId,
}
);
@@ -12,9 +12,6 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.viewer.me.invalidate();
},
async onSettled() {
await utils.viewer.public.i18n.invalidate();
},
});
return (
@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import { useCallback, useState } from "react";
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
@@ -1,4 +1,5 @@
import classNames from "classnames";
// eslint-disable-next-line no-restricted-imports
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@@ -50,7 +51,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const pathname = usePathname();
const router = useRouter();
const { t } = useLocale();
const { data: session, update } = useSession();
const { update } = useSession();
const {
currentUsername,
setCurrentUsername = noop,
@@ -94,7 +95,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
debouncedApiCall(inputUsernameValue);
}, [debouncedApiCall, inputUsernameValue]);
const utils = trpc.useContext();
const updateUsername = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
@@ -104,9 +104,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
onError: (error) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.viewer.public.i18n.invalidate();
},
});
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
@@ -1,4 +1,5 @@
import classNames from "classnames";
// eslint-disable-next-line no-restricted-imports
import { debounce, noop } from "lodash";
import { useSession } from "next-auth/react";
import type { RefCallback } from "react";
@@ -24,7 +25,7 @@ interface ICustomUsernameProps {
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
const { t } = useLocale();
const { data: session, update } = useSession();
const { update } = useSession();
const {
currentUsername,
@@ -65,8 +66,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
}
}, [inputUsernameValue, debouncedApiCall, currentUsername]);
const utils = trpc.useContext();
const updateUsernameMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
@@ -77,9 +76,6 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
onError: (error) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.viewer.public.i18n.invalidate();
},
});
const ActionButtons = () => {
@@ -1,35 +1,26 @@
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { PremiumTextfield } from "./PremiumTextfield";
import { UsernameTextfield } from "./UsernameTextfield";
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
interface UsernameAvailabilityFieldProps {
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
}
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
return organization
? organization.slug
? getOrgFullDomain(organization.slug, { protocol: false })
: organization.metadata && organization.metadata.requestedSlug
? getOrgFullDomain(organization.metadata.requestedSlug, { protocol: false })
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
}
export const getUsernameAvailabilityComponent = (isPremium: boolean) => {
if (isPremium)
return dynamic(() => import("./PremiumTextfield").then((m) => m.PremiumTextfield), { ssr: false });
return dynamic(() => import("./UsernameTextfield").then((m) => m.UsernameTextfield), { ssr: false });
};
export const UsernameAvailabilityField = ({
onSuccessMutation,
@@ -49,7 +40,12 @@ export const UsernameAvailabilityField = ({
},
});
const usernamePrefix = useUserNamePrefix(user.organization);
const UsernameAvailability = getUsernameAvailabilityComponent(!IS_SELF_HOSTED && !user.organization?.id);
const orgBranding = useOrgBranding();
const usernamePrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
return (
<Controller
@@ -65,7 +61,8 @@ export const UsernameAvailabilityField = ({
setInputUsernameValue={onChange}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
addOnLeading={usernamePrefix}
disabled={!!user.organization?.id}
addOnLeading={`${usernamePrefix}/`}
/>
);
}}
+1
View File
@@ -0,0 +1 @@
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
+4 -7
View File
@@ -1,7 +1,6 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import type { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { useSession } from "next-auth/react";
import { SessionProvider, useSession } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client";
import type { SSRConfig } from "next-i18next";
import { appWithTranslation } from "next-i18next";
@@ -21,7 +20,7 @@ import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
import { useClientViewerI18n } from "@components/I18nLanguageHandler";
const I18nextAdapter = appWithTranslation<
NextJsAppProps<SSRConfig> & {
@@ -69,11 +68,9 @@ type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
const { i18n, locale } = useViewerI18n().data ?? {
locale: "en",
};
const clientViewerI18n = useClientViewerI18n(props.router.locales || []);
const { i18n, locale } = clientViewerI18n.data || {};
const passedProps = {
...props,
-53
View File
@@ -1,53 +0,0 @@
import parser from "accept-language-parser";
import type { IncomingMessage } from "http";
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { Maybe } from "@calcom/trpc/server";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { i18n } = require("@calcom/config/next-i18next.config");
export function getLocaleFromHeaders(req: IncomingMessage): string {
let preferredLocale: string | null | undefined;
if (req.headers["accept-language"]) {
preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]) as Maybe<string>;
}
return preferredLocale ?? i18n.defaultLocale;
}
export const getOrSetUserLocaleFromHeaders = async (
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"]
): Promise<string> => {
const { default: prisma } = await import("@calcom/prisma");
const session = await getServerSession({ req, res });
const preferredLocale = getLocaleFromHeaders(req);
if (session?.user?.id) {
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
locale: true,
},
});
if (user?.locale) {
return user.locale;
}
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
locale: preferredLocale,
},
});
}
return preferredLocale;
};
+4
View File
@@ -3,6 +3,7 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
const os = require("os");
const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { version } = require("./package.json");
const { i18n } = require("./next-i18next.config");
const {
orgHostPath,
@@ -14,6 +15,9 @@ const {
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
// To be able to use the version in the app without having to import package.json
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
// So we can test deploy previews preview
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL;
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.0",
"version": "3.2.5",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -68,6 +68,7 @@
"@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0",
"async": "^3.2.4",
"bcp-47-match": "^2.0.3",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
"dotenv-cli": "^6.0.0",
@@ -166,6 +167,7 @@
"msw": "^0.42.3",
"postcss": "^8.4.18",
"tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.6",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
+1
View File
@@ -75,6 +75,7 @@ export default function Custom404() {
)}`
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isSuccessPage = pathname?.startsWith("/booking");
+9 -2
View File
@@ -36,6 +36,7 @@ import { ssrInit } from "@server/lib/ssr";
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
const { t } = useLocale();
@@ -52,7 +53,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
orgSlug: _orgSlug,
...query
} = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/*
const telemetry = useTelemetry();
@@ -82,6 +82,10 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
profile: { name: `${profile.name}`, image: null },
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !profile.allowSEOIndexing,
nofollow: !profile.allowSEOIndexing,
}}
/>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
@@ -93,7 +97,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
)}>
<div className="mb-8 text-center">
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
<h1 className="font-cal text-emphasis mb-1 text-3xl">
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
{profile.name}
{user.verified && (
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
@@ -214,6 +218,7 @@ export type UserPageProps = {
theme: string | null;
brandColor: string;
darkBrandColor: string;
allowSEOIndexing: boolean;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
themeBasis: string | null;
@@ -276,6 +281,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
away: true,
verified: true,
allowDynamicBooking: true,
allowSEOIndexing: true,
},
});
@@ -315,6 +321,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
allowSEOIndexing: user.allowSEOIndexing ?? true,
};
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
+5
View File
@@ -30,6 +30,7 @@ export default function Type({
booking,
away,
isBrandingHidden,
isSEOIndexable,
rescheduleUid,
entity,
duration,
@@ -41,6 +42,7 @@ export default function Type({
eventSlug={slug}
rescheduleUid={rescheduleUid ?? undefined}
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
entity={entity}
/>
<Booker
@@ -128,6 +130,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
isSEOIndexable: true,
themeBasis: null,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
@@ -154,6 +157,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
select: {
away: true,
hideBranding: true,
allowSEOIndexing: true,
},
});
@@ -199,6 +203,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
entity: eventData.entity,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
isSEOIndexable: user?.allowSEOIndexing,
themeBasis: username,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
+1 -1
View File
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
{(!IS_PRODUCTION || process.env.VERCEL_ENV === "preview") && (
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
// eslint-disable-next-line @next/next/no-sync-scripts
<script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
+7 -34
View File
@@ -8,7 +8,7 @@ import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { IS_CALCOM } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInOrg } from "@calcom/lib/validateUsernameInOrg";
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -65,41 +65,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Token expired" });
}
if (foundToken?.teamId) {
const isValidUsername = await validateUsernameInOrg(username, foundToken?.teamId);
if (!isValidUsername) {
return res.status(409).json({ message: "Username already taken" });
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
if (!teamUserValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
} else {
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
const userValidation = await validateUsername(username, userEmail);
if (!userValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
@@ -111,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.teamId,
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
@@ -1,10 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/client";
@@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// If user has 2fa enabled, check if body.code is correct
const isValidToken = authenticator.check(req.body.code, secret);
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
@@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const isValidToken = authenticator.check(req.body.code, secret);
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
}
-48
View File
@@ -7,54 +7,6 @@ import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
const language = { translate: t, locale: "en" };
const evt = {
type: "30min",
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
name: "Pro Example",
email: "pro@example.com",
timeZone: "Europe/London",
language,
},
attendees: [
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
recurringEvent: null,
appsStatus: [
{
appName: "Outlook Calendar",
type: "office365_calendar",
success: 1,
failures: 0,
errors: [],
warnings: [],
},
{
appName: "Google Meet",
type: "conferencing",
success: 0,
failures: 1,
errors: [],
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
},
],
};
res.statusCode = 200;
@@ -0,0 +1,4 @@
import appBasecamp3 from "@calcom/app-store/basecamp3/trpc-router";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
export default createNextApiHandler(appBasecamp3);
@@ -40,9 +40,11 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
}>
<div className="mb-16">
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
{apps.map((app) => {
return <AppCard key={app.slug} app={app} />;
})}
{apps
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
.map((app) => {
return <AppCard key={app.slug} app={app} />;
})}
</div>
</div>
</Shell>
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { debounce } from "lodash";
import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
+44 -4
View File
@@ -1,14 +1,16 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useCallback } from "react";
import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { ShellMain } from "@calcom/features/shell/Shell";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { EmptyScreen, showToast } from "@calcom/ui";
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
import { Clock } from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
@@ -129,14 +131,52 @@ const WithQuery = withQuery(trpc.viewer.availability.list as any);
export default function AvailabilityPage() {
const { t } = useLocale();
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
const createQueryString = useCallback(
(name: string, value: string) => {
const params = new URLSearchParams(searchParams ?? undefined);
params.set(name, value);
return params.toString();
},
[searchParams]
);
return (
<div>
<ShellMain
heading={t("availability")}
hideHeadingOnMobile
subtitle={t("configure_availability")}
CTA={<NewScheduleButton />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
CTA={
<div className="flex gap-2">
<ToggleGroup
className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
if (!value) return;
router.push(`${pathname}?${createQueryString("type", value)}`);
}}
options={[
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
]}
/>
<NewScheduleButton />
</div>
}>
{searchParams?.get("type") === "team" ? (
<AvailabilitySliderTable />
) : (
<WithQuery
success={({ data }) => <AvailabilityList {...data} />}
customLoader={<SkeletonLoader />}
/>
)}
</ShellMain>
</div>
);
+24 -8
View File
@@ -23,12 +23,9 @@ import {
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import {
getBookingFieldsWithSystemFields,
SMS_REMINDER_NUMBER_FIELD,
} from "@calcom/features/bookings/lib/getBookingFields";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import { APP_NAME } from "@calcom/lib/constants";
import {
@@ -96,7 +93,7 @@ const querySchema = z.object({
});
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const { t, i18n } = useLocale();
const router = useRouter();
const routerQuery = useRouterQuery();
const pathname = usePathname();
@@ -113,7 +110,8 @@ export default function Success(props: SuccessProps) {
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
(attendee) => attendee.email === email
)?.timeZone;
const tz = isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : props.tz ? props.tz : timeZone();
const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone();
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
@@ -480,6 +478,21 @@ export default function Success(props: SuccessProps) {
</div>
</>
)}
{props.paymentStatus && (
<>
<div className="mt-3 font-medium">
{props.paymentStatus.paymentOption === "HOLD"
? t("complete_your_booking")
: t("payment")}
</div>
<div className="col-span-2 mb-2 mt-3">
{new Intl.NumberFormat(i18n.language, {
style: "currency",
currency: props.paymentStatus.currency,
}).format(props.paymentStatus.amount / 100.0)}
</div>
</>
)}
{bookingInfo?.description && (
<>
<div className="mt-9 font-medium">{t("additional_notes")}</div>
@@ -715,7 +728,7 @@ export default function Success(props: SuccessProps) {
</div>
{isGmail && (
<Alert
className="main -mb-20 mt-4 inline-block ltr:text-right rtl:text-right sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
className="main -mb-20 mt-4 inline-block ltr:text-left rtl:text-right sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
severity="warning"
message={
<div>
@@ -1117,6 +1130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: {
success: true,
refunded: true,
currency: true,
amount: true,
paymentOption: true,
},
});
+1 -1
View File
@@ -179,7 +179,7 @@ export default function Bookings() {
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
@@ -132,6 +132,7 @@ export type FormValues = {
bookingFields: z.infer<typeof eventTypeBookingFields>;
availability?: AvailabilityOption;
bookerLayouts: BookerLayoutSettings;
multipleDurationEnabled: boolean;
};
export type CustomInputParsed = typeof customInputSchema._output;
@@ -278,6 +279,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
.filter((slug) => slug !== eventType.slug) ?? [],
},
})),
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
};
}, [eventType, periodDates, metadata]);
@@ -375,7 +377,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
bookerLayouts,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
...input
} = values;
@@ -406,6 +411,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("seats_and_no_show_fee_error"));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
@@ -463,6 +469,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
...input
} = values;
@@ -488,6 +496,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
+7 -5
View File
@@ -737,7 +737,9 @@ const EventTypeListHeading = ({
</span>
)}
{profile?.slug && (
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
<Link
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
className="text-subtle block text-xs">
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
</Link>
)}
@@ -830,13 +832,13 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
const Main = ({
status,
error,
errorMessage,
data,
filters,
}: {
status: string;
data: GetByViewerResponse;
error: any;
errorMessage?: string;
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
}) => {
const isMobile = useMediaQuery("(max-width: 768px)");
@@ -848,7 +850,7 @@ const Main = ({
}
if (status === "error") {
return <Alert severity="error" title="Something went wrong" message={error.message} />;
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
}
const isFilteredByOnlyOneItem =
@@ -945,7 +947,7 @@ const EventTypesPage = () => {
title="Event Types"
description="Create events to share for people to book on your calendar."
/>
<Main data={data} status={status} error={error} filters={filters} />
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
</ShellMain>
);
};
@@ -1,9 +1,9 @@
import UnverifiedOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/unverifiedOrgPage";
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";
const Page = UnverifiedOrgsPage as CalPageWrapper;
const Page = AdminOrgsPage as CalPageWrapper;
Page.PageWrapper = PageWrapper;
export default Page;
@@ -57,7 +57,7 @@ const ApiKeysView = () => {
<div>
{isLoading ? null : data?.length ? (
<>
<div className="mb-8 mt-6 rounded-md border">
<div className="border-subtle mb-8 mt-6 rounded-md border">
{data.map((apiKey, index) => (
<ApiKeyListItem
key={apiKey.id}
+27 -17
View File
@@ -1,3 +1,4 @@
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
@@ -20,8 +21,6 @@ import {
TimezoneSelect,
} from "@calcom/ui";
import { withQuery } from "@lib/QueryCell";
import PageWrapper from "@components/PageWrapper";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
@@ -45,11 +44,6 @@ interface GeneralViewProps {
user: RouterOutputs["viewer"]["me"];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WithQuery = withQuery(trpc.viewer.public.i18n as any, undefined, {
trpc: { context: { skipBatch: true } },
});
const GeneralQueryView = () => {
const { t } = useLocale();
@@ -58,30 +52,26 @@ const GeneralQueryView = () => {
if (!user) {
throw new Error(t("something_went_wrong"));
}
return (
<WithQuery
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
/>
);
return <GeneralView user={user} localeProp={user.locale} />;
};
const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
const utils = trpc.useContext();
const { t } = useLocale();
const { update } = useSession();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
// Invalidate our previous i18n cache
await utils.viewer.public.i18n.invalidate();
onSuccess: async (res) => {
await utils.viewer.me.invalidate();
reset(getValues());
showToast(t("settings_updated_successfully"), "success");
update(res);
},
onError: () => {
showToast(t("error_updating_settings"), "error");
},
onSettled: async () => {
await utils.viewer.public.i18n.invalidate();
await utils.viewer.me.invalidate();
},
});
@@ -116,6 +106,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
},
allowDynamicBooking: user.allowDynamicBooking ?? true,
allowSEOIndexing: user.allowSEOIndexing ?? true,
},
});
const {
@@ -124,6 +115,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
getValues,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form
form={formMethods}
@@ -225,6 +217,24 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
)}
/>
</div>
<div className="mt-8">
<Controller
name="allowSEOIndexing"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("seo_indexing")}
description={t("allow_seo_indexing")}
checked={formMethods.getValues("allowSEOIndexing")}
onCheckedChange={(checked) => {
formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true });
}}
/>
)}
/>
</div>
<Button
loading={mutation.isLoading}
disabled={isDisabled}
+17 -15
View File
@@ -77,26 +77,25 @@ type FormValues = {
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: _session, update } = useSession();
const { update } = useSession();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => {
await update(res);
showToast(t("settings_updated_successfully"), "success");
if (res.signOutUser && tempFormValues) {
if (res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else {
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
}
// signout user only in case of password reset
if (res.signOutUser && tempFormValues && res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else {
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
utils.viewer.shouldVerifyEmail.invalidate();
}
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
setConfirmAuthEmailChangeWarningDialogOpen(false);
update(res);
setTempFormValues(null);
},
onError: () => {
@@ -325,7 +324,10 @@ const ProfileView = () => {
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
</div>
<DialogFooter showDivider>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
<Button
color="primary"
loading={confirmPasswordMutation.isLoading}
onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
<DialogClose />
@@ -345,7 +347,7 @@ const ProfileView = () => {
<DialogFooter>
<Button
color="primary"
disabled={updateProfileMutation.isLoading}
loading={updateProfileMutation.isLoading}
onClick={(e) => onConfirmAuthEmailChange(e)}>
{t("confirm")}
</Button>
+31 -23
View File
@@ -14,6 +14,7 @@ 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -130,10 +131,11 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
<TextField
addOnLeading={
orgSlug
? getOrgFullDomain(orgSlug, { protocol: false })
? getOrgFullDomain(orgSlug, { protocol: true })
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
}
{...register("username")}
disabled={!!orgSlug}
required
/>
<EmailField
@@ -192,8 +194,6 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
};
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || flags["disable-signup"]) {
console.log({ flag: flags["disable-signup"] });
return {
notFound: true,
};
@@ -210,6 +210,20 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
where: {
token,
},
include: {
team: {
select: {
metadata: true,
parentId: true,
parent: {
select: {
slug: true,
},
},
slug: true,
},
},
},
});
if (!verificationToken || verificationToken.expires < new Date()) {
@@ -249,41 +263,35 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
let username = guessUsernameFromEmail(verificationToken.identifier);
const orgInfo = await prisma.user.findFirst({
where: {
email: verificationToken?.identifier,
},
select: {
organization: {
select: {
slug: true,
metadata: true,
},
},
},
});
const tokenTeam = {
...verificationToken?.team,
metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
};
const userOrgMetadata = teamMetadataSchema.parse(orgInfo?.organization?.metadata ?? {});
// Detect if the team is an org by either the metadata flag or if it has a parent team
const isOrganization = tokenTeam.metadata?.isOrganization || tokenTeam?.parentId !== null;
// If we are dealing with an org, the slug may come from the team itself or its parent
const orgSlug = isOrganization
? tokenTeam.slug || tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug
: null;
if (!IS_SELF_HOSTED) {
// Org context shouldn't check if a username is premium
if (!IS_SELF_HOSTED && !isOrganization) {
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
const { available, suggestion } = await checkPremiumUsername(username);
username = available ? username : suggestion || username;
}
// Transform all + to - in username
username = username.replace(/\+/g, "-");
return {
props: {
...props,
token,
prepopulateFormValues: {
email: verificationToken.identifier,
username,
username: slugify(username),
},
orgSlug: (orgInfo?.organization?.slug || userOrgMetadata?.requestedSlug) ?? null,
orgSlug,
},
};
};
+12 -4
View File
@@ -182,7 +182,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
size="lg"
/>
</div>
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider">
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
{team.parent && `${team.parent.name} `}
{teamName}
</p>
@@ -199,19 +199,27 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<SubTeams />
) : (
<>
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{(showMembers.isOn || !team.eventTypes.length) &&
(team.isPrivate ? (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
</div>
) : (
<Team team={team} />
))}
{!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes />
{!team.hideBookATeamMember && (
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{(!team.hideBookATeamMember || team.isPrivate) && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
+6 -7
View File
@@ -1,6 +1,7 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { installAppleCalendar } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -10,10 +11,9 @@ test.describe("App Store - Authed", () => {
test("Browse apple-calendar and try to install", async ({ page, users }) => {
const pro = await users.create();
await pro.apiLogin();
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
await installAppleCalendar(page);
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
});
@@ -31,9 +31,8 @@ test.describe("App Store - Authed", () => {
test.describe("App Store - Unauthed", () => {
test("Browse apple-calendar and try to install", async ({ page }) => {
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.click('[data-testid="install-app-button"]');
await installAppleCalendar(page);
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();
});
});
+35
View File
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { installAppleCalendar } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
const APPLE_CALENDAR_EMAIL = process.env.E2E_TEST_APPLE_CALENDAR_EMAIL!;
const APPLE_CALENDAR_PASSWORD = process.env.E2E_TEST_APPLE_CALENDAR_PASSWORD!;
const SHOULD_SKIP_TESTS = !APPLE_CALENDAR_EMAIL || !APPLE_CALENDAR_PASSWORD;
test.describe("Apple Calendar", () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(SHOULD_SKIP_TESTS, "Skipping due to missing the testing credentials");
test("Should be able to install and login on Apple Calendar", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await installAppleCalendar(page);
await expect(page.locator('[data-testid="apple-calendar-form"]')).toBeVisible();
await page.fill('[data-testid="apple-calendar-email"]', APPLE_CALENDAR_EMAIL);
await page.fill('[data-testid="apple-calendar-password"]', APPLE_CALENDAR_PASSWORD);
await page.click('[data-testid="apple-calendar-login-button"]');
await expect(page.getByText("Apple Calendar")).toBeVisible();
await expect(page.getByText(APPLE_CALENDAR_EMAIL)).toBeVisible();
});
});
+7 -13
View File
@@ -1,5 +1,4 @@
import { expect } from "@playwright/test";
import type { Messages } from "mailhog";
import { randomString } from "@calcom/lib/random";
@@ -8,6 +7,7 @@ import {
bookFirstEvent,
bookOptinEvent,
bookTimeSlot,
expectEmailsToHaveSubject,
selectFirstAvailableTimeSlotNextMonth,
testEmail,
testName,
@@ -41,19 +41,13 @@ test.describe("free user", () => {
// Make sure we're navigated to the success page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const { title: eventTitle } = await user.getFirstEventAsOwner();
// TODO: follow DRY
const emailsOrganiserReceived = await emails.search(user.email, "to");
const emailsBookerReceived = await emails.search(bookerObj.email, "to");
expect(emailsOrganiserReceived?.total).toBe(1);
expect(emailsBookerReceived?.total).toBe(1);
const [organizerFirstEmail] = (emailsOrganiserReceived as Messages).items;
const [bookerFirstEmail] = (emailsBookerReceived as Messages).items;
const emailSubject = `${eventTitle} between ${user.name} and ${bookerObj.name}`;
expect(organizerFirstEmail.subject).toBe(emailSubject);
expect(bookerFirstEmail.subject).toBe(emailSubject);
await expectEmailsToHaveSubject({
emails,
organizer: user,
booker: bookerObj,
eventTitle,
});
await page.goto(bookingUrl);
// book same time spot again
+113
View File
@@ -0,0 +1,113 @@
import { expect } from "@playwright/test";
import { BookingStatus } from "@calcom/prisma/client";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
test.afterEach(({ users }) => users.deleteAll());
test.describe("Bookings", () => {
test.describe("Upcoming bookings", () => {
test("show attendee bookings and organizer bookings in asc order by startDate", async ({
page,
users,
bookings,
}) => {
const firstUser = await users.create();
const secondUser = await users.create();
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
title: "Booking as organizer",
bookingsFixture: bookings,
// Create a booking 3 days from today
relativeDate: 3,
organizer: firstUser,
organizerEventType: firstUser.eventTypes[0],
attendees: [
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
],
});
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
title: "Booking as attendee",
bookingsFixture: bookings,
organizer: secondUser,
// Booking created 2 days from today
relativeDate: 2,
organizerEventType: secondUser.eventTypes[0],
attendees: [
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
],
});
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
await firstUser.apiLogin();
await page.goto(`/bookings/upcoming`);
const upcomingBookings = page.locator('[data-testid="upcoming-bookings"]');
const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0);
const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1);
await expect(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
).toBeVisible();
await expect(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
).toBeVisible();
});
});
});
async function createBooking({
bookingsFixture,
organizer,
organizerEventType,
attendees,
/**
* Relative date from today
* 0 means today
* 1 means tomorrow
*/
relativeDate = 0,
durationMins = 30,
title,
}: {
bookingsFixture: Fixtures["bookings"];
organizer: {
id: number;
username: string | null;
};
organizerEventType: {
id: number;
};
attendees: {
name: string;
email: string;
timeZone: string;
}[];
relativeDate?: number;
durationMins?: number;
title: string;
}) {
const DAY_MS = 24 * 60 * 60 * 1000;
const bookingDurationMs = durationMins * 60 * 1000;
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
title,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
attendees: {
createMany: {
data: [...attendees],
},
},
});
}
+43
View File
@@ -1,3 +1,4 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug);
});
test("edit first event", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test.describe("Different Locations Tests", () => {
test("can add Attendee Phone Number location and book with it", async ({ page }) => {
await gotoFirstEventType(page);
await selectAttendeePhoneNumber(page);
await saveEventType(page);
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator(`[data-fob-field-name="location"] input`).fill("9199999999");
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("text=+19199999999")).toBeVisible();
});
});
});
});
const selectAttendeePhoneNumber = async (page: Page) => {
const locationOptionText = "Attendee Phone Number";
await page.locator("#location-select").click();
await page.locator(`text=${locationOptionText}`).click();
};
async function gotoFirstEventType(page: Page) {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
}
async function saveEventType(page: Page) {
await page.locator("[data-testid=update-eventtype]").click();
}
async function gotoBookingPage(page: Page) {
const previewLink = await page.locator("[data-testid=preview-button]").getAttribute("href");
await page.goto(previewLink ?? "");
}
+6 -3
View File
@@ -19,9 +19,12 @@ export const createBookingsFixture = (page: Page) => {
username: string | null,
eventTypeId = -1,
{
title = "",
rescheduled = false,
paid = false,
status = "ACCEPTED",
startTime,
endTime,
attendees = {
create: {
email: "attendee@example.com",
@@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
const booking = await prisma.booking.create({
data: {
uid: uid,
title: "30min",
startTime: startDate,
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
title: title || "30min",
startTime: startTime || startDate,
endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
user: {
connect: {
id: userId,
+57 -16
View File
@@ -5,6 +5,7 @@ import { hashSync as hash } from "bcryptjs";
import type { API } from "mailhog";
import dayjs from "@calcom/dayjs";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
@@ -117,7 +118,7 @@ const createTeamAndAddUser = async (
};
// creates a user fixture instance and stores the collection
export const createUsersFixture = (page: Page, emails: API, workerInfo: WorkerInfo) => {
export const createUsersFixture = (page: Page, emails: API | undefined, workerInfo: WorkerInfo) => {
const store = { users: [], page } as { users: UserFixture[]; page: typeof page };
return {
create: async (
@@ -331,19 +332,22 @@ export const createUsersFixture = (page: Page, emails: API, workerInfo: WorkerIn
await page.goto("/auth/logout");
},
deleteAll: async () => {
const emailMessageIds: string[] = [];
const ids = store.users.map((u) => u.id);
for (const user of store.users) {
const emailMessages = await emails.search(user.email);
if (emailMessages && emailMessages.count > 0) {
emailMessages.items.forEach((item) => {
emailMessageIds.push(item.ID);
});
if (emails) {
const emailMessageIds: string[] = [];
for (const user of store.users) {
const emailMessages = await emails.search(user.email);
if (emailMessages && emailMessages.count > 0) {
emailMessages.items.forEach((item) => {
emailMessageIds.push(item.ID);
});
}
}
for (const id of emailMessageIds) {
await emails.deleteMessage(id);
}
}
for (const id of emailMessageIds) {
await emails.deleteMessage(id);
}
await prisma.user.deleteMany({ where: { id: { in: ids } } });
store.users = [];
},
@@ -416,8 +420,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
getPaymentCredential: async () => getPaymentCredential(store.page),
setupEventWithPrice: async (eventType: Pick<Prisma.EventType, "id">) =>
setupEventWithPrice(eventType, store.page),
bookAndPaidEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
bookAndPaidEvent(user, eventType, store.page),
bookAndPayEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
bookAndPayEvent(user, eventType, store.page),
makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page),
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
debug: async (message: string | Record<string, JSONValue>) => {
@@ -427,6 +431,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
});
},
delete: async () => await prisma.user.delete({ where: { id: store.user.id } }),
confirmPendingPayment: async () => confirmPendingPayment(store.page),
};
};
@@ -437,12 +442,18 @@ type CustomUserOptsKeys = "username" | "password" | "completedOnboarding" | "loc
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
timeZone?: TimeZoneEnum;
eventTypes?: SupportedTestEventTypes[];
// ignores adding the worker-index after username
useExactUsername?: boolean;
};
// creates the actual user in the db.
const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): PrismaType.UserCreateInput => {
// build a unique name for our user
const uname = `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
const uname =
opts?.useExactUsername && opts?.username
? opts.username
: `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
return {
username: uname,
name: opts?.name,
@@ -468,6 +479,36 @@ const createUser = (workerInfo: WorkerInfo, opts?: CustomUserOpts | null): Prism
};
};
async function confirmPendingPayment(page: Page) {
await page.waitForURL(new RegExp("/booking/*"));
const url = page.url();
const params = new URLSearchParams(url.split("?")[1]);
const id = params.get("payment_intent");
if (!id) throw new Error(`Payment intent not found in url ${url}`);
const payload = JSON.stringify(
{ type: "payment_intent.succeeded", data: { object: { id } }, account: "e2e_test" },
null,
2
);
const signature = stripe.webhooks.generateTestHeaderString({
payload,
secret: process.env.STRIPE_WEBHOOK_SECRET as string,
});
const response = await page.request.post("/api/integrations/stripepayment/webhook", {
data: payload,
headers: { "stripe-signature": signature },
});
if (response.status() !== 200) throw new Error(`Failed to confirm payment. Response: ${response.text()}`);
}
// login using a replay of an E2E routine.
export async function login(
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
@@ -501,7 +542,7 @@ export async function apiLogin(
.then((json) => json.csrfToken);
const data = {
email: user.email ?? `${user.username}@example.com`,
password: user.password ?? user.username!,
password: user.password ?? user.username,
callbackURL: "http://localhost:3000/",
redirect: "false",
json: "true",
@@ -519,7 +560,7 @@ export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id"
await page.getByTestId("update-eventtype").click();
}
export async function bookAndPaidEvent(
export async function bookAndPayEvent(
user: Pick<Prisma.User, "username">,
eventType: Pick<Prisma.EventType, "slug">,
page: Page
+56 -6
View File
@@ -1,6 +1,7 @@
import { expect } from "@playwright/test";
import type Prisma from "@prisma/client";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
@@ -41,7 +42,7 @@ test.describe("Stripe integration", () => {
await user.getPaymentCredential();
await user.setupEventWithPrice(eventType);
await user.bookAndPaidEvent(eventType);
await user.bookAndPayEvent(eventType);
// success
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
@@ -78,7 +79,7 @@ test.describe("Stripe integration", () => {
await user.getPaymentCredential();
await user.setupEventWithPrice(eventType);
await user.bookAndPaidEvent(eventType);
await user.bookAndPayEvent(eventType);
// Rescheduling the event
await Promise.all([page.waitForURL("/booking/*"), page.click('[data-testid="reschedule-link"]')]);
@@ -92,8 +93,57 @@ test.describe("Stripe integration", () => {
await user.makePaymentUsingStripe();
});
todo("Payment should confirm pending payment booking");
todo("Payment should trigger a BOOKING_PAID webhook");
todo("Paid booking should be able to be cancelled");
todo("Cancelled paid booking should be refunded");
test("Paid booking should be able to be cancelled", async ({ page, users }) => {
const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
await user.setupEventWithPrice(eventType);
await user.bookAndPayEvent(eventType);
await page.click('[data-testid="cancel"]');
await page.click('[data-testid="confirm_cancel"]');
await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible();
});
test.describe("When event is paid and confirmed", () => {
let user: Awaited<ReturnType<Fixtures["users"]["create"]>>;
let eventType: Prisma.EventType;
test.beforeEach(async ({ page, users }) => {
user = await users.create();
eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
await user.setupEventWithPrice(eventType);
await user.bookAndPayEvent(eventType);
await user.confirmPendingPayment();
});
test("Cancelled paid booking should be refunded", async ({ page, users, request }) => {
await page.click('[data-testid="cancel"]');
await page.click('[data-testid="confirm_cancel"]');
await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible();
await expect(page.getByText("This booking payment has been refunded")).toBeVisible();
});
test("Payment should confirm pending payment booking", async ({ page, users }) => {
await page.goto("/bookings/upcoming");
const paidBadge = page.locator('[data-testid="paid_badge"]').first();
await expect(paidBadge).toBeVisible();
expect(await paidBadge.innerText()).toBe("Paid");
});
todo("Payment should trigger a BOOKING_PAID webhook");
todo("Paid and confirmed booking should be able to be rescheduled");
});
});
+8 -3
View File
@@ -3,6 +3,7 @@ import { test as base } from "@playwright/test";
import type { API } from "mailhog";
import mailhog from "mailhog";
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import type { ExpectedUrlDetails } from "../../../../playwright.config";
@@ -21,7 +22,7 @@ export interface Fixtures {
getActionFiredDetails: ReturnType<typeof createGetActionFiredDetails>;
servers: ReturnType<typeof createServersFixture>;
prisma: typeof prisma;
emails: API;
emails?: API;
}
declare global {
@@ -71,7 +72,11 @@ export const test = base.extend<Fixtures>({
await use(prisma);
},
emails: async ({}, use) => {
const mailhogAPI = mailhog();
await use(mailhogAPI);
if (IS_MAILHOG_ENABLED) {
const mailhogAPI = mailhog();
await use(mailhogAPI);
} else {
await use(undefined);
}
},
});
+45
View File
@@ -2,7 +2,9 @@ import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import type { IncomingMessage, ServerResponse } from "http";
import { createServer } from "http";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { API, Messages } from "mailhog";
import { test } from "./fixtures";
@@ -183,3 +185,46 @@ export async function gotoRoutingLink({
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
await new Promise((resolve) => setTimeout(resolve, 1000));
}
export async function installAppleCalendar(page: Page) {
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
}
export async function getEmailsReceivedByUser({
emails,
userEmail,
}: {
emails?: API;
userEmail: string;
}): Promise<Messages | null> {
if (!emails) return null;
return emails.search(userEmail, "to");
}
export async function expectEmailsToHaveSubject({
emails,
organizer,
booker,
eventTitle,
}: {
emails?: API;
organizer: { name?: string | null; email: string };
booker: { name: string; email: string };
eventTitle: string;
}) {
if (!emails) return null;
const emailsOrganizerReceived = await getEmailsReceivedByUser({ emails, userEmail: organizer.email });
const emailsBookerReceived = await getEmailsReceivedByUser({ emails, userEmail: booker.email });
expect(emailsOrganizerReceived?.total).toBe(1);
expect(emailsBookerReceived?.total).toBe(1);
const [organizerFirstEmail] = (emailsOrganizerReceived as Messages).items;
const [bookerFirstEmail] = (emailsBookerReceived as Messages).items;
const emailSubject = `${eventTitle} between ${organizer.name ?? "Nameless"} and ${booker.name}`;
expect(organizerFirstEmail.subject).toBe(emailSubject);
expect(bookerFirstEmail.subject).toBe(emailSubject);
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { expect } from "@playwright/test";
import { authenticator } from "otplib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
import { test } from "./lib/fixtures";
@@ -141,7 +142,7 @@ test.describe("2FA Tests", async () => {
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
let token = authenticator.generate(secret);
if (!noRetry && !authenticator.check(token, secret)) {
if (!noRetry && !totpAuthenticatorCheck(token, secret)) {
console.log("Token expired, Renerating.");
// Maybe token was just about to expire, try again just once more
token = authenticator.generate(secret);
@@ -202,7 +202,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
await addQuestionAndSave({
page,
question: {
name: "how_are_you",
name: "how-are-you",
type: "Address",
label: "How are you?",
placeholder: "I'm fine, thanks",
@@ -214,7 +214,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
const allFieldsLocator = await expectSystemFieldsToBeThereOnBookingPage({ page });
const userFieldLocator = allFieldsLocator.nth(5);
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
await expect(userFieldLocator.locator('[name="how-are-you"]')).toBeVisible();
// There are 2 labels right now. Will be one in future. The second one is hidden
expect(await getLabelText(userFieldLocator)).toBe("How are you?");
await expect(userFieldLocator.locator("input")).toBeVisible();
@@ -223,18 +223,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
name: "how-are-you",
page,
});
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
await expect(formBuilderFieldLocator).toBeHidden();
});
});
await test.step("Show Question Again", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
name: "how-are-you",
page,
});
});
@@ -242,7 +242,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
await test.step('Try to book without providing "How are you?" response', async () => {
await doOnFreshPreview(page, context, async (page) => {
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expectErrorToBeThereFor({ page, name: "how_are_you" });
await expectErrorToBeThereFor({ page, name: "how-are-you" });
});
});
@@ -260,18 +260,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
page,
context,
async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
await expect(formBuilderFieldLocator).toBeVisible();
expect(
await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")
await formBuilderFieldLocator.locator('[name="how-are-you"]').getAttribute("placeholder")
).toBe("I'm fine, thanks");
expect(await getLabelText(formBuilderFieldLocator)).toBe("How are you?");
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
await formBuilderFieldLocator.locator('[name="how-are-you"]').fill("I am great!");
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
expect(
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
await page.locator('[data-testid="field-response"][data-fob-field="how-are-you"]').innerText()
).toBe("I am great!");
await waitFor(() => {
@@ -287,7 +287,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
label: "email_address",
value: "booker@example.com",
},
how_are_you: {
"how-are-you": {
label: "How are you?",
value: "I am great!",
},
@@ -303,7 +303,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
});
expect(payload.userFieldsResponses).toMatchObject({
how_are_you: {
"how-are-you": {
label: "How are you?",
value: "I am great!",
},
+42
View File
@@ -135,6 +135,48 @@ test.describe("Teams", () => {
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email
});
test("Can create team with same name as user", async ({ page, users }) => {
// Name to be used for both user and team
const uniqueName = "test-unique-name";
const ownerObj = { username: uniqueName, name: uniqueName, useExactUsername: true };
const user = await users.create(ownerObj);
await user.apiLogin();
await page.goto("/teams");
await test.step("Can create team with same name", async () => {
// Click text=Create Team
await page.locator("text=Create Team").click();
await page.waitForURL("/settings/teams/new");
// Fill input[name="name"]
await page.locator('input[name="name"]').fill(uniqueName);
// Click text=Continue
await page.locator("text=Continue").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
// Click text=Continue
await page.locator("text=Publish team").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
});
await test.step("Can access user and team with same slug", async () => {
// Go to team page and confirm name
const teamUrl = `/team/${uniqueName}`;
await page.goto(teamUrl);
await page.waitForURL(teamUrl);
await expect(page.locator("[data-testid=team-name]")).toHaveText(uniqueName);
// Go to user page and confirm name
const userUrl = `/${uniqueName}`;
await page.goto(userUrl);
await page.waitForURL(userUrl);
await expect(page.locator("[data-testid=name-title]")).toHaveText(uniqueName);
// cleanup team
const team = await prisma.team.findFirst({ where: { slug: uniqueName } });
await prisma.team.delete({ where: { id: team?.id } });
});
});
todo("Create a Round Robin with different leastRecentlyBooked hosts");
todo("Reschedule a Collective EventType booking");
todo("Reschedule a Round Robin EventType booking");
+1
View File
@@ -184,6 +184,7 @@ test.describe("BOOKING_REJECTED", async () => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
+82 -82
View File
@@ -13,15 +13,15 @@
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"verify_email_subject": "{{appName}}: تأكيد حسابك",
"check_your_email": "تحقق من بريدك الإلكتروني",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل قابلية تسليم للبريد الإلكتروني والرزنامة من {{appName}}.",
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والرزنامة",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
"verify_email_email_button": "تأكيد البريد الإلكتروني",
"verify_email_email_body": "الرجاء تأكيد عنوان بريدك الإلكتروني بالنقر على الزر أدناه.",
"verify_email_email_link_text": "إليك الرابط في حال لم تكن ترغب بالنقر على أي أزرار:",
"verify_email_email_body": "الرجاء تأكيد عنوان بريدك الإلكتروني بالنقر فوق الزر أدناه.",
"verify_email_email_link_text": "إليك الرابط في حال لم تكن ترغب بالنقر فوق أي أزرار:",
"email_sent": "تم إرسال الرسالة الإلكترونية بنجاح",
"event_declined_subject": "تم الرفض: {{title}} في {{date}}",
"event_cancelled_subject": "تم إلغاء: {{title}} في {{date}}",
"event_cancelled_subject": "تم الإلغاء: {{title}} في {{date}}",
"event_request_declined": "تم رفض طلب الحدث الخاص بك",
"event_request_declined_recurring": "تم رفض طلب الحدث المتكرر الخاص بك",
"event_request_cancelled": "تم إلغاء حدثك المُجَدْوَل",
@@ -89,7 +89,7 @@
"event_has_been_rescheduled": "تم التحديث - تم إعادة جدولة الحدث الخاص بك",
"request_reschedule_subtitle": "ألغى {{organizer}} الحجز وطلب منك اختيار حجز في وقت آخر.",
"request_reschedule_title_organizer": "لقد طلبت من {{attendee}} إعادة جدولة موعد",
"request_reschedule_subtitle_organizer": "لقد ألغيت الحجز، ويجب أن يختار {{attendee}} وقتًا جديداً للحجز معك.",
"request_reschedule_subtitle_organizer": "لقد ألغيت الحجز، ويجب أن يختار {{attendee}} وقتًا جديدًا للحجز معك.",
"rescheduled_event_type_subject": "تم إرسال طلب لإعادة الجدولة: {{eventType}} مع {{name}} في {{date}}",
"requested_to_reschedule_subject_attendee": "الإجراء المطلوب إعادة الجدولة: يُرجى حجز وقت جديد لمدة {{eventType}} مع {{name}}",
"hi_user_name": "مرحبا {{name}}",
@@ -129,8 +129,8 @@
"team_upgrade_banner_description": "شكراً لك على تجربة خطة الفريق الجديدة التي نقدمها. لقد لاحظنا أن فريقك \"{{teamName}}\" بحاجة إلى الترقية.",
"upgrade_banner_action": "قم بالترقية هنا",
"team_upgraded_successfully": "تمت ترقية فريقك بنجاح!",
"org_upgrade_banner_description": "شكراً لك على تجربة الخطة الجديدة لمؤسستنا التي نقدمها. لقد لاحظنا أن مؤسستك \"{{teamName}}\" بحاجة إلى الترقية.",
"org_upgraded_successfully": "تمت ترقية مؤسستك بنجاح!",
"org_upgrade_banner_description": "شكرًا لك على تجربة خطة Organization الجديدة التي نقدمها. لقد لاحظنا أن منظمتك \"{{teamName}}\" بحاجة إلى الترقية.",
"org_upgraded_successfully": "تمت ترقية منظمتك بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا،",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
@@ -248,7 +248,7 @@
"organization_general_description": "إدارة الإعدادات للغة فريقك ومنطقته الزمنية",
"few_sentences_about_yourself": "اكتب بضع جمل عن نفسك. سيظهر هذا على صفحة عنوان URL الشخصية لديك.",
"nearly_there": "أوشكت على الانتهاء!",
"nearly_there_instructions": "وفي نهاية المطاف: يساعدك كتابة وصف موجز عنك ووضع صورة في الحصول على عمليات حجز وأن يعرف الأشخاص مع من سيحجزون حقًا.",
"nearly_there_instructions": "وأخيرًا، يساعدك الوصف الموجز عنك والصورة في الحصول على الحجوزات والسماح للأشخاص بمعرفة الشخص الذين يحجزون معه.",
"set_availability_instructions": "حدد الفترات الزمنية التي تكون متاحًا فيها بشكل متكرر. يمكنك لاحقًا تحديد المزيد منها وربطها مع تقاويم مختلفة.",
"set_availability": "تحديد الوقت الذي تكون فيه متاحًا",
"continue_without_calendar": "المتابعة من دون تقويم",
@@ -304,18 +304,18 @@
"success": "تم بنجاح",
"failed": "فشل",
"password_has_been_reset_login": "تمت إعادة تعيين كلمة المرور الخاصة بك. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور التي تم إنشاؤها حديثًا.",
"layout": "تخطيط",
"layout": "التخطيط",
"bookerlayout_default_title": "العرض الافتراضي",
"bookerlayout_description": "يمكنك اختيار خيارات متعددة، ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض.",
"bookerlayout_user_settings_title": "تخطيط الحجز",
"bookerlayout_user_settings_description": "يمكنك تحديد خيارات متعددة ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض. يمكن إبطال هذا بحسب وضع كل فعالية.",
"bookerlayout_user_settings_description": "يمكنك تحديد خيارات متعددة ويمكن لمن يقومون بالحجز لديك تبديل طرق العرض. يمكن تجاوز هذا على أساس كل حدث.",
"bookerlayout_month_view": "شهر",
"bookerlayout_week_view": "أسبوعياً",
"bookerlayout_week_view": "أسبوعيًا",
"bookerlayout_column_view": "عمود",
"bookerlayout_error_min_one_enabled": "يجب تمكين تخطيط واحد على الأقل.",
"bookerlayout_error_default_not_enabled": "التخطيط الذي حددته كطريقة عرض افتراضية ليس جزءاً من التخطيطات التي تم تمكينها.",
"bookerlayout_error_unknown_layout": "التخطيط الذي اخترته ليس تخطيطاً صالحاً.",
"bookerlayout_override_global_settings": "يمكنك إدارة هذا لجميع أنواع فعالياتك في <2>إعدادات/المظهر</2> أو <6>إبطال لهذا الحدث فقط</6>.",
"bookerlayout_error_default_not_enabled": "التخطيط الذي حددته كطريقة عرض افتراضية ليس جزءًا من التخطيطات التي تم تمكينها.",
"bookerlayout_error_unknown_layout": "التخطيط الذي اخترته ليس تخطيطًا صالحًا.",
"bookerlayout_override_global_settings": "يمكنك إدارة هذا لجميع أنواع الأحداث لديك في <2>الإعدادات/المظهر</2> أو <6>التجاوز لهذا الحدث فقط</6>.",
"unexpected_error_try_again": "حدث خطأ غير متوقع. حاول مجددًا.",
"sunday_time_error": "وقت غير صالح يوم الأحد",
"monday_time_error": "وقت غير صالح يوم الاثنين",
@@ -337,9 +337,9 @@
"cancelling_event_recurring": "إن هذا الحدث أحد الأمثلة على حدث متكرر.",
"cancelling_all_recurring": "هذه هي كل الأمثلة المتبقية في الحدث المتكرر.",
"error_with_status_code_occured": "حدث خطأ برمز الحالة {{status}}.",
"error_event_type_url_duplicate": "يوجد بالفعل نوع فعالية مع هذا الرابط.",
"error_event_type_unauthorized_create": "أنت غير قادر على إنشاء هذه الفعالية",
"error_event_type_unauthorized_update": "أنت غير قادر على تعديل هذه الفعالية",
"error_event_type_url_duplicate": "يوجد بالفعل نوع حدث بهذا الرابط.",
"error_event_type_unauthorized_create": "أنت غير قادر على إنشاء هذا الحدث",
"error_event_type_unauthorized_update": "أنت غير قادر على تعديل هذا الحدث",
"error_workflow_unauthorized_create": "أنت غير قادر على إنشاء سير العمل هذا",
"error_schedule_unauthorized_create": "أنت غير قادر على إنشاء هذا الجدول الزمني",
"booking_already_cancelled": "تم إلغاء هذا الحجز بالفعل",
@@ -352,7 +352,7 @@
"no_status_bookings_yet_description": "ليست لديك عمليات حجز في حالة {{status}}.‏ {{description}}",
"event_between_users": "{{eventName}} بين {{host}} و{{attendeeName}}",
"bookings": "عمليات الحجز",
"booking_not_found": "الحجز غير موجود",
"booking_not_found": "لم يتم العثور على الحجز",
"bookings_description": "اطلع على الأحداث القادمة والسابقة المحجوزة لديك من خلال روابط أنواع الحدث.",
"upcoming_bookings": "بمجرد أن يحجز شخص ما موعدًا معك، سيظهر هنا.",
"recurring_bookings": "بمجرد أن يحجز شخص ما اجتماعًا متكررًا معك، سيظهر هنا.",
@@ -413,8 +413,8 @@
"team_webhooks": "فريق ويبهوكس",
"create_new_webhook_to_account": "إنشاء ويب هوك جديد لحسابك",
"new_webhook": "ويب هوك جديد",
"receive_cal_meeting_data": "تلقَّ بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الفعلي، عندما تتم جدولة حدث ما أو يتم إلغاؤه.",
"receive_cal_event_meeting_data": "تلقي بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الحقيقي، عندما تتم جدولة حدث ما أو إلغاؤه.",
"receive_cal_meeting_data": "احصل على بيانات اجتماع {{appName}} على رابط محدد، في الوقت الفعلي، عند جدولة حدث أو إلغائه.",
"receive_cal_event_meeting_data": "احصل على بيانات اجتماع {{appName}} على رابط محدد، في الوقت الفعلي، عند جدولة حدث أو إلغائه.",
"responsive_fullscreen_iframe": "إطار iframe سريع الاستجابة بكامل الشاشة",
"loading": "يجري التحميل...",
"deleting": "جارٍ الحذف...",
@@ -554,7 +554,7 @@
"member": "العضو",
"number_member_one": "{{count}} عضو",
"number_member_other": "{{count}} من الأعضاء",
"number_selected": "{{count}} تم اختيارها",
"number_selected": "تم تحديد {{count}}",
"owner": "المالك",
"admin": "المشرف",
"administrator_user": "المستخدم المسؤول",
@@ -872,16 +872,16 @@
"no_category_apps_description_automation": "إضافة تطبيق أتمتة لاستخدامه",
"no_category_apps_description_other": "أضف أي نوع آخر من التطبيقات للقيام بأي شيء",
"no_category_apps_description_web3": "إضافة تطبيق web3 لصفحات الحجز لديك",
"no_category_apps_description_messaging": "إضافة تطبيق مراسلة لإعداد التنبيهات والمذكرات المخصصة",
"no_category_apps_description_messaging": "إضافة تطبيق مراسلة لإعداد الإشعارات والتذكيرات المخصصة",
"no_category_apps_description_crm": "إضافة تطبيق CRM لتتبع من قابلته",
"installed_app_calendar_description": "قم بتعيين التقويمات للتحقق من وجود تعارضات لمنع الحجوزات المزدوجة.",
"installed_app_payment_description": "قم بإعداد أي خدمات معالجة دفع تود استخدامها عند التحصيل من العملاء.",
"installed_app_analytics_description": "تهيئة أي تطبيقات تستخدم لصفحات الحجز الخاصة بك",
"installed_app_other_description": "جميع تطبيقاتك المثبتة من الفئات الأخرى.",
"installed_app_conferencing_description": "تهيئة أي تطبيقات المؤتمرات لاستخدامها",
"installed_app_conferencing_description": "تكوين تطبيقات المؤتمرات المراد استخدامها",
"installed_app_automation_description": "تهيئة تطبيقات الأتمتة التي سيتم استخدامها",
"installed_app_web3_description": "تهيئة تطبيقات web3 التي يمكن استخدامها لصفحات الحجز لديك",
"installed_app_messaging_description": "تكوين أي تطبيقات الرسائل لاستخدامها لإعداد الإشعارات والمذكرات المخصصة",
"installed_app_messaging_description": "تكوين تطبيقات المراسلة المراد استخدامها لإعداد الإشعارات والتذكيرات المخصصة",
"installed_app_crm_description": "تهيئة تطبيقات CRM التي يمكن استخدامها لتتبع من قابلته",
"analytics": "التحليلات",
"empty_installed_apps_headline": "لم يتم تثبيت أي تطبيق",
@@ -908,7 +908,7 @@
"create_events_on": "إنشاء أحداث في",
"enterprise_license": "هذه هي ميزة للمؤسسات",
"enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.",
"enterprise_license_development": "يمكنك اختبار هذه الميزة في وضع التطوير. لاستخدام الإنتاج يرجى جعل المسؤول يذهب إلى <2>/auth/setup</2> لإدخال مفتاح الترخيص.",
"enterprise_license_development": "يمكنك اختبار هذه الميزة في وضع التطوير. لاستخدام الإنتاج، يرجى مطالبة المسؤول بالذهاب إلى <2>/auth/setup</2> لإدخال مفتاح الترخيص.",
"missing_license": "الترخيص مفقود",
"signup_requires": "يلزم ترخيص تجاري",
"signup_requires_description": "لا تقدم شركة {{companyName}} حاليًا إصدارًا مجانيًا مفتوح المصدر لصفحة التسجيل. للحصول على حق الوصول الكامل إلى مكونات التسجيل، يجب أن تحصل على ترخيص تجاري. للاستخدام الشخصي، نوصي باستخدام منصة Prisma Data أو أي واجهة أخرى من واجهات Postgres لإنشاء الحسابات.",
@@ -1001,9 +1001,9 @@
"user_impersonation_heading": "انتحال شخصية مستخدم",
"user_impersonation_description": "يسمح لفريق الدعم الخاص بنا بتسجيل الدخول مؤقتًا نيابة عنك لمساعدتنا على حل أي مشاكل تبلغ عنها بسرعة.",
"team_impersonation_description": "يسمح لمالكي/مشرفي فريقك بتسجيل الدخول مؤقتًا نيابة عنك.",
"make_team_private": "اجعل الفريق سري",
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا.",
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء فريق سري.",
"make_team_private": "اجعل الفريق خاصًا",
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا الإعداد.",
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء الفريق السري.",
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
@@ -1071,18 +1071,18 @@
"event_location": "موقع الحدث",
"reschedule_optional": "سبب إعادة الجدولة (اختياري)",
"reschedule_placeholder": "أخبر الآخرين لماذا تحتاج إلى إعادة جدولة",
"event_cancelled": "تم إلغاء هذه الفعالية",
"event_cancelled": "تم إلغاء هذا الحدث",
"emailed_information_about_cancelled_event": "لقد أرسلنا إليك وإلى الحاضرين الآخرين بريداً إلكترونياً لإعلامهم.",
"this_input_will_shown_booking_this_event": "سيُعرَض هذا المُدخَل عند حجز هذا الحدث",
"meeting_url_in_conformation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
"number_provided": "سيتم توفير رقم الهاتف",
"before_event_trigger": "قبل بدء الحدث",
"event_cancelled_trigger": "متى تم إلغاء هذه الفعالية",
"event_cancelled_trigger": "متى تم إلغاء هذا الحدث",
"new_event_trigger": "متى تم حجز الحدث الجديد",
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
"sms_attendee_action": "إرسال رسالة نصية قصيرة إلى واحد من الحضور",
"sms_attendee_action": "إرسال رسالة نصية قصيرة إلى أحد الحضور",
"sms_number_action": "إرسال رسالة نصية قصيرة إلى رقم محدد",
"workflows": "سير العمل",
"new_workflow_btn": "سير عمل جديد",
@@ -1331,9 +1331,9 @@
"event_one": "حدث",
"event_other": "أحداث",
"profile_team_description": "إدارة إعدادات الملف الشخصي لفريقك",
"profile_org_description": "إدارة الإعدادات لملف مؤسستك",
"profile_org_description": "إدارة الإعدادات لملف منظمتك",
"members_team_description": "المستخدمين الموجودين في المجموعة",
"organization_description": "إدارة المشرفين والأعضاء في مؤسستك",
"organization_description": "إدارة المسؤولين والأعضاء في منظمتك",
"team_url": "رابط الفريق",
"team_members": "أعضاء الفريق",
"more": "المزيد",
@@ -1479,10 +1479,10 @@
"yearly": "سنويًا",
"checkout": "الدفع",
"your_team_disbanded_successfully": "تم حل فريقك بنجاح",
"your_org_disbanded_successfully": "تم حل مؤسستك بنجاح",
"your_org_disbanded_successfully": "تم حل منظمتك بنجاح",
"error_creating_team": "خطأ في إنشاء فريق",
"you": "أنت",
"resend_email": "إعادة إرسال رسالة إلكترونية",
"resend_email": "إعادة إرسال رسالة البريد الإلكتروني",
"member_already_invited": "تمت دعوة العضو بالفعل",
"enter_email_or_username": "أدخل بريد إلكتروني أو اسم مستخدم",
"team_name_taken": "هذا الاسم مأخوذ بالفعل",
@@ -1548,7 +1548,7 @@
"no_apps": "لا توجد تطبيقات مفعلة لهذا المثال من Cal",
"no_apps_configured": "لم يتم تكوين أي تطبيق بعد",
"enable_in_settings": "يمكنك تمكين التطبيقات في الإعدادات",
"please_contact_admin": "الرجاء الاتصال بالمشرف",
"please_contact_admin": "يرجى الاتصال بالمسؤول",
"apps_settings": "إعدادات التطبيقات",
"fill_this_field": "يرجى ملء هذا الحقل",
"options": "خيارات",
@@ -1652,10 +1652,10 @@
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_user_cta": "عرض الدعوة",
"email_no_user_invite_heading": "تم دعوتك للانضمام إلى {{appName}} {{entity}}",
"email_no_user_invite_heading": "تمت دعوتك للانضمام إلى {{appName}} {{entity}}",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه {{teamName}} لدى {{entity}} على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات من دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدولة سريعة خالية من الإجهاد مع {{entity}}.",
"email_user_invite_subheading": "لقد دعاك {{invitedBy}} للانضمام إلى {{entity}} `{{teamName}}` على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدول زمني خالٍ من التوتر مع {{entity}} في لمح البصر.",
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
"email_no_user_step_three": "تعيين أوقاتك المتاحة",
@@ -1666,8 +1666,8 @@
"scheduler": "{Scheduler}",
"no_workflows": "لا يوجد أي سير عمل",
"change_filter": "قم بتغيير عامل التصفية لمشاهدة سير العمل الشخصي وفريقك.",
"change_filter_common": "تغيير عامل الفرز لرؤية النتائج.",
"no_results_for_filter": "لا توجد نتائج لعامل الفرز",
"change_filter_common": "تغيير عامل التصفية لرؤية النتائج.",
"no_results_for_filter": "لا توجد نتائج لعامل التصفية",
"recommended_next_steps": "الخطوات التالية الموصى بها",
"create_a_managed_event": "إنشاء نوع من الأحداث المُدارة",
"meetings_are_better_with_the_right": "الاجتماعات أفضل مع وجود أعضاء الفريق المناسبين. قم بدعوتهم الآن.",
@@ -1678,7 +1678,7 @@
"no_longer_attending": "لن تحضر هذا الحدث",
"attendee_no_longer_attending_subject": "أحد الحضور لن يحضر {{title}} في {{date}}",
"attendee_no_longer_attending": "أحد الحضور لن يحضر الحدث الخاص بك",
"attendee_no_longer_attending_subtitle": "تم إلغاء الحدث {{name}}، وهو ما يعني توافر معقد شاغر في هذا التوقيت",
"attendee_no_longer_attending_subtitle": "تم إلغاء {{name}}. هذا يعني أنه تم فتح مقعد لهذه الفترة الزمنية",
"create_event_on": "إنشاء حدث في",
"create_routing_form_on": "إنشاء نموذج توجيه على",
"default_app_link_title": "تعيين رابط تطبيق افتراضي",
@@ -1690,7 +1690,7 @@
"booking_questions_title": "أسئلة حول الحجز",
"booking_questions_description": "يمكنك تخصيص الأسئلة المطروحة في صفحة الحجز",
"add_a_booking_question": "اطرح سؤالاً",
"identifier": "مُعرِّف",
"identifier": "المُعرِّف",
"duplicate_email": "البريد الإلكتروني مكرر",
"booking_with_payment_cancelled": "لم يعد الدفع لهذا الحدث ممكنًا",
"booking_with_payment_cancelled_already_paid": "استرداد المبلغ المدفوع لهذا الحجز قيد التنفيذ.",
@@ -1733,14 +1733,14 @@
"no_responses_yet": "لا توجد ردود بعد",
"no_routes_defined": "لا توجد مسارات محددة",
"this_will_be_the_placeholder": "سيكون هذا هو العنصر النائب",
"error_booking_event": "حدث خطأ عند حجز الفعالية، الرجاء تحديث الصفحة والمحاولة ثانيةً",
"error_booking_event": "حدث خطأ عند حجز الحدث، يرجى تحديث الصفحة والمحاولة ثانيةً",
"timeslot_missing_title": "لم يتم تحديد فترة زمنية",
"timeslot_missing_description": "يرجى تحديد فترة زمنية لحجز الحدث.",
"timeslot_missing_cta": "حدد فترة زمنية",
"switch_monthly": "التبديل إلى العرض الشهري",
"switch_weekly": "التبديل إلى العرض الأسبوعي",
"switch_multiday": "التبديل إلى العرض اليومي",
"switch_columnview": "التبديل إلى العرض العمود",
"switch_columnview": "التبديل إلى عرض العمود",
"num_locations": "{{num}} من خيارات الموقع",
"select_on_next_step": "حدد في الخطوة التالية",
"this_meeting_has_not_started_yet": "لم يبدأ هذا الاجتماع بعد",
@@ -1795,7 +1795,7 @@
"least_booked_members": "الأعضاء الأقل حجزًا",
"events_created": "تم إنشاء الأحداث",
"events_completed": "تم اكتمال الأحداث",
"events_cancelled": "تم إلغاء الفعاليات",
"events_cancelled": "تم إلغاء الأحداث",
"events_rescheduled": "تمت إعادة جدولة الأحداث",
"from_last_period": "من آخر فترة",
"from_to_date_period": "من: {{startDate}} إلى: {{endDate}}",
@@ -1834,13 +1834,13 @@
"invite_link_copied": "تم نسخ رابط الدعوة",
"invite_link_deleted": "تم حذف رابط الدعوة",
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
"link_expires_after": "روابط تم تعيينها للانتهاء بعد...",
"link_expires_after": "تم تعيين الروابط للانتهاء بعد...",
"one_day": "1 يوم",
"seven_days": "7 أيام",
"thirty_days": "30 يوم",
"thirty_days": "30 يومًا",
"three_months": "3 أشهر",
"one_year": "1 سنة",
"team_invite_received": "تم دعوتك للانضمام إلى {{teamName}}",
"team_invite_received": "تمت دعوتك للانضمام إلى {{teamName}}",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "أنت على وشك تحصيل مبلغ {{amount, currency}} من أحد الحضور. هل أنت متأكد من أنك تريد المتابعة؟",
"charge_attendee": "تحصيل مبلغ {{amount, currency}} من أحد الحضور",
@@ -1849,7 +1849,7 @@
"email_invite_team_bulk": "تم دعوة {{userCount}} من المستخدمين",
"error_collecting_card": "خطأ في التحصيل من البطاقة",
"image_size_limit_exceed": "يجب ألا يتجاوز حجم الصورة التي تم تحميلها 5 ميغابايت",
"unauthorized_workflow_error_message": "{{errorCode}}: أنت غير مخوّل لك بتمكين أو تعطيل سير العمل هذا",
"unauthorized_workflow_error_message": "{{errorCode}}: غير مصرح لك بتمكين سير العمل هذا أو تعطيله",
"inline_embed": "التضمين",
"load_inline_content": "تحميل نوع الحدث مع محتوى آخر من موقعكبشكل مباشر.",
"floating_pop_up_button": "زر منبثق عائم",
@@ -1871,66 +1871,66 @@
"first_event_type_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من الأحداث",
"install_app_on": "تثبيت التطبيق على",
"create_for": "إنشاء من أجل",
"setup_organization": "إعداد مؤسسة",
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة، ومسارات عمل وأنواع فعاليات مع جدولة دورية وجماعية.",
"organization_banner_title": "إدارة المنظمات مع فرق متعددة",
"setup_organization": "إعداد منظمة",
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة ومهام سير العمل وأنواع الأحداث باستخدام الجدولة الدوارة والجماعية.",
"organization_banner_title": "إدارة المنظمات ذات الفرق المتعددة",
"set_up_your_organization": "إعداد منظمتك",
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الفعاليات المشتركة والتطبيقات ومسارات العمل والمزيد.",
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الأحداث المشتركة والتطبيقات ومهام سير العمل والمزيد.",
"must_enter_organization_name": "يجب إدخال اسم المنظمة",
"must_enter_organization_admin_email": "يجب إدخال عنوان البريد الإلكتروني لمؤسستك",
"admin_email": "عنوان البريد الإلكتروني لمؤسستك",
"must_enter_organization_admin_email": "يجب إدخال عنوان البريد الإلكتروني لمنظمتك",
"admin_email": "عنوان البريد الإلكتروني لمنظمتك",
"admin_username": "اسم المستخدم للمسؤول",
"organization_name": "اسم المنظمة",
"organization_url": "رابط المنظمة",
"organization_verify_header": "تأكيد البريد الإلكتروني لمؤسستك",
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد مؤسستك.",
"additional_url_parameters": "مقاييس URL إضافية",
"about_your_organization": "حول مؤسستك",
"about_your_organization_description": "المنظمات هي بيئات مشتركة فيها يمكنك إنشاء فرق متعدد مع أعضاء مشتركين، وأنواع الفعاليات، والتطبيقات، ومسارات العمل والمزيد.",
"organization_verify_header": "تأكيد البريد الإلكتروني لمنظمتك",
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
"additional_url_parameters": "معلمات الرابط الإضافية",
"about_your_organization": "حول منظمتك",
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.",
"create_your_teams": "إنشاء فرقك",
"create_your_teams_description": "البدء في الجدولة معاً بإضافة أعضاء فريقك إلى مؤسستك",
"invite_organization_admins": "قم بدعوة مشرفي مؤسستك",
"invite_organization_admins_description": "سيتمكن هؤلاء المشرفون من الوصول إلى جميع الفرق في مؤسستك. يمكنك إضافة مشرفين وأعضاء في وقت لاحق.",
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
"invite_organization_admins": "دعوة مشرفي منظمتك",
"invite_organization_admins_description": "سيتمكن هؤلاء المسؤولون من الوصول إلى جميع الفرق في منظمتك. يمكنك إضافة مسؤولي الفريق والأعضاء لاحقًا.",
"set_a_password": "تعيين كلمة مرور",
"set_a_password_description": "سيؤدي هذا إلى إنشاء حساب مستخدم جديد مع البريد الإلكتروني لمؤسستك وكلمة المرور هذه.",
"set_a_password_description": "سيؤدي هذا إلى إنشاء حساب مستخدم جديد بالبريد الإلكتروني لمنظمتك وكلمة المرور هذه.",
"organization_logo": "شعار المنظمة",
"organization_about_description": "بضع جمل عن منظمتك. ستظهر على الصفحة المتاحجة للعموم لمنظمتك.",
"ill_do_this_later": "سأفعل هذا لاحقاً",
"organization_about_description": "بضع جمل عن منطمتك. سيظهر هذا في صفحة الملف الشخصي العامة لمنظمتك.",
"ill_do_this_later": "سأفعل هذا لاحقًا",
"verify_your_email": "تأكيد بريدك الإلكتروني",
"enter_digit_code": "أدخل الرمز المكون من 6 أرقام الذي أرسلناه إلى {{email}}",
"verify_email_organization": "تأكيد بريدك الإلكتروني لإنشاء مؤسسة",
"verify_email_organization": "أكد بريدك الإلكتروني لإنشاء منظمة",
"code_provided_invalid": "الرمز المقدم غير صالح، حاول ثانيةً",
"email_already_used": "البريد الإلكتروني مستخدم بالفعل",
"organization_admin_invited_heading": "تم دعوتك للانضمام إلى {{orgName}}",
"organization_admin_invited_body": "انضم إلى فريقك في {{orgName}} وابدأ بالتركيز على الاجتماع، وليس عقد الاجتماعات!",
"duplicated_slugs_warning": "لا يمكن إنشاء الفرق التالية بسبب التكرار: {{slugs}}",
"organization_admin_invited_heading": "تمت دعوتك للانضمام إلى {{orgName}}",
"organization_admin_invited_body": "انضم إلى فريقك في {{orgName}} وابدأ الانشغال بالاجتماعاتك، وليس بكيفية الإعداد لها!",
"duplicated_slugs_warning": "لا يمكن إنشاء الفرق التالية بسبب تكرار الكلمات اللطيفة (Slug): {{slugs}}",
"team_names_empty": "لا يمكن أن تكون أسماء الفرق فارغة",
"team_names_repeated": "لا يمكن تكرار أسماء الفرق",
"user_belongs_organization": "المستخدم ينتمي إلى منظمة",
"org_no_teams_yet": "هذه المنظمة ليس لديها فرق بعد",
"org_no_teams_yet_description": "إذا كنت مسؤول، تأكد من إنشاء فرق لعرضها هنا.",
"set_up": "إعداد",
"set_up_your_profile": "إعداد ملفك التعريفي",
"set_up": "الإعداد",
"set_up_your_profile": "إعداد ملفك الشخصي",
"set_up_your_profile_description": "دع الناس يعرفون من أنت داخل {{orgName}}، ومتى يتعاملون مع الرابط العام الخاص بك.",
"my_profile": "ملفي التعريفي",
"my_settings": "إعداداتي",
"my_profile": "الملف الشخصي",
"my_settings": "الإعدادات",
"crm": "CRM",
"messaging": "المراسلة",
"sender_id_info": "الاسم أو الرقم المبين كمرسل لرسالة نصية قصيرة (بعض البلدان لا تسمح بأن تكون هويات المرسل أبجدية رقمية)",
"org_admins_can_create_new_teams": "يمكن فقط لمشرف مؤسستك إنشاء فرق جديدة",
"sender_id_info": "الاسم أو الرقم الظاهر على أنه مرسل الرسالة النصية القصيرة (بعض البلدان لا تسمح بمعرفات المرسل الأبجدية الرقمية)",
"org_admins_can_create_new_teams": "يمكن فقط لمسؤول منظمتك إنشاء فرق جديدة",
"google_new_spam_policy": "يمكن أن تمنعك سياسة Google الجديدة للرسائل غير المرغوب فيها من تلقي أي إشعارات بالبريد الإلكتروني والتقويم حول هذا الحجز.",
"resolve": "حل",
"no_organization_slug": "حدث خطأ أثناء إنشاء فرق لهذه المؤسسة. ربط الرابط مفقود.",
"no_organization_slug": "حدث خطأ أثناء إنشاء فرق لهذه المنظمة. الكلمة اللطيفة للرابط مفقودة.",
"org_name": "اسم المنظمة",
"org_url": "رابط المنظمة",
"copy_link_org": "نسخ الرابط إلى المنظمة",
"404_the_org": "المنظمة",
"404_the_team": "الفريق",
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمؤسستك",
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
"404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي",
"insights_all_org_filter": "الكل",
"insights_team_filter": "الفريق: {{teamName}}",
"insights_user_filter": "المستخدم: {{userName}}",
"insights_subtitle": "عرض رؤى الحجز عبر فعالياتك",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف خيوطك الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"insights_subtitle": "عرض Insights الحجز عبر أحداثك",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+27 -27
View File
@@ -21,7 +21,7 @@
"verify_email_email_link_text": "Zde je odkaz pro případ, že nemáte rádi klikání na tlačítka:",
"email_sent": "E-mail byl odeslán",
"event_declined_subject": "Odmítnuto: {{title}} v {{date}}",
"event_cancelled_subject": "Zrušeno: {{title}} ({{date}})",
"event_cancelled_subject": "Zrušeno: {{title}} dne {{date}}",
"event_request_declined": "Žádost o událost byla zamítnuta",
"event_request_declined_recurring": "Žádost o opakovanou událost byla zamítnuta",
"event_request_cancelled": "Vaše plánovaná událost byla zrušena",
@@ -89,7 +89,7 @@
"event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy",
"request_reschedule_subtitle": "{{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný termín.",
"request_reschedule_title_organizer": "Požádali jste uživatele {{attendee}} o změnu časového plánu",
"request_reschedule_subtitle_organizer": "Zrušili jste rezervaci a účastník {{attendee}} by si s vámi měl rezervovat nový čas.",
"request_reschedule_subtitle_organizer": "Zrušili jste rezervaci a účastník {{attendee}} by si s vámi měl zarezervovat nový čas.",
"rescheduled_event_type_subject": "Žádost o přesunutí schůzky: {{eventType}} s {{name}} dne {{date}}",
"requested_to_reschedule_subject_attendee": "Akce vyžadovaná k přeplánování: zarezervujte si nový čas pro událost {{eventType}} s osobou {{name}}",
"hi_user_name": "Hezký den, {{name}}",
@@ -129,7 +129,7 @@
"team_upgrade_banner_description": "Děkujeme vám za vyzkoušení našeho nového týmového plánu. Všimli jsme si, že váš tým „{{teamName}}“ je nutné upgradovat.",
"upgrade_banner_action": "Upgradovat zde",
"team_upgraded_successfully": "Váš tým byl úspěšně přešel na vyšší verzi!",
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli tarif naší organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli náš tarif Organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
"org_upgraded_successfully": "Vaše organizace byla upgradována!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Zdravíme,",
@@ -248,7 +248,7 @@
"organization_general_description": "Správa nastavení jazyka a časového pásma vašeho týmu",
"few_sentences_about_yourself": "Pár vět o vás. Budou se zobrazovat na URL vaší osobní stránky.",
"nearly_there": "Téměř hotovo!",
"nearly_there_instructions": "Poslední věc... stručný popis o vás a fotografie vám pomůžou získat rezervace a lidi aspoň budou vědět, koho si rezervují.",
"nearly_there_instructions": "Poslední věc... pár slov o vás a fotografie vám pomůžou získat rezervace a lidi aspoň budou vědět, koho si rezervují.",
"set_availability_instructions": "Určete si časové intervaly, kdy jste pravidelně dostupní. Později můžete vytvořit další a přiřadit je různým kalendářům.",
"set_availability": "Nastavte, jak jste dostupní",
"continue_without_calendar": "Pokračovat bez kalendáře",
@@ -314,8 +314,8 @@
"bookerlayout_column_view": "Sloupec",
"bookerlayout_error_min_one_enabled": "Musí být povoleno alespoň jedno rozvržení.",
"bookerlayout_error_default_not_enabled": "Rozvržení, které jste vybrali jako výchozí zobrazení, není součástí povolených rozvržení.",
"bookerlayout_error_unknown_layout": "Vámi vybrané rozvržení není platné rozvržení.",
"bookerlayout_override_global_settings": "Tohle lze nastavit pro všechny typy událostí v <2>nastavení / vzhledu</2> nebo <6>nadefinovat pouze pro tuto událost</6>.",
"bookerlayout_error_unknown_layout": "Vámi vybrané rozvržení není platné.",
"bookerlayout_override_global_settings": "Toto lze nastavit pro všechny typy událostí v <2>nastavení / vzhled</2> nebo <6>nadefinovat pouze pro tuto událost</6>.",
"unexpected_error_try_again": "Došlo k neočekávané chybě. Zkuste to znovu.",
"sunday_time_error": "Neplatný čas v neděli",
"monday_time_error": "Neplatný čas v pondělí",
@@ -341,7 +341,7 @@
"error_event_type_unauthorized_create": "Tuto událost nelze vytvořit",
"error_event_type_unauthorized_update": "Tuto událost nelze upravit",
"error_workflow_unauthorized_create": "Tento pracovní postup nelze vytvořit",
"error_schedule_unauthorized_create": "Nejste schopni vytvořit tento plán",
"error_schedule_unauthorized_create": "Tento plán nelze vytvořit",
"booking_already_cancelled": "Tato rezervace již byla zrušena",
"booking_already_accepted_rejected": "Tato rezervace již byla přijata nebo zamítnuta",
"go_back_home": "Přejít na úvod",
@@ -352,7 +352,7 @@
"no_status_bookings_yet_description": "Nemáte žádné {{status}} rezervace. {{description}}",
"event_between_users": "{{eventName}} mezi {{host}} a {{attendeeName}}",
"bookings": "Rezervace",
"booking_not_found": "Rezervace nenalezena",
"booking_not_found": "Rezervace nebyla nalezena",
"bookings_description": "Zobrazte si nadcházející a proběhlé události rezervovaní prostřednictvím odkazu na typ události.",
"upcoming_bookings": "Jakmile si někdo rezervuje váš čas, zobrazí se to zde.",
"recurring_bookings": "Jakmile si někdo s vámi zarezervuje opakovanou schůzku, zobrazí se to zde.",
@@ -398,12 +398,12 @@
"browse_api_documentation": "Procházejte naši API dokumentaci",
"leverage_our_api": "Pro plnou kontrolu a přizpůsobení využijte naše API.",
"create_webhook": "Vytvořit webhook",
"booking_cancelled": "Rezervace zrušena",
"booking_cancelled": "Rezervace byla zrušena",
"booking_rescheduled": "Rezervace přesunuta",
"recording_ready": "Odkaz ke stažení záznamu je připraven",
"booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace zamítnuta",
"booking_requested": "Rezervace vyžádána",
"booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Byla vyžádána rezervace",
"meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán",
"event_triggers": "Eventy na základě akce",
@@ -867,7 +867,7 @@
"no_category_apps": "Žádné aplikace v kategorii {{category}}",
"all_apps": "Všechny aplikace",
"no_category_apps_description_calendar": "Přidejte aplikaci kalendáře, ať máte přehled v zájmu prevence dvojích rezervací",
"no_category_apps_description_conferencing": "Zkuste přidat aplikaci pro videokonference s klienty.",
"no_category_apps_description_conferencing": "Zkuste přidat aplikaci pro videokonference s klienty",
"no_category_apps_description_payment": "Přidejte platební aplikaci, která usnadní provádění transakcí mezi vámi a vašimi klienty",
"no_category_apps_description_analytics": "Přidejte analytickou aplikaci pro vaše rezervační stránky",
"no_category_apps_description_automation": "Přidejte aplikaci pro automatizaci",
@@ -1002,8 +1002,8 @@
"user_impersonation_heading": "Přizpůsobení uživatele",
"user_impersonation_description": "Náš tým podpory se bude moci dočasně přihlásit místo vás, aby nám pomohl rychle vyřešit jakékoli problémy, které nám nahlásíte.",
"team_impersonation_description": "Umožňuje vašim vlastníkům/správcům týmu dočasně se přihlásit jako vy.",
"make_team_private": "Soukromý tým",
"make_team_private_description": "Po zapnutí této funkce nebudou členové vašeho týmu vidět ostatní členy týmu.",
"make_team_private": "Nastavit tým jako soukromý",
"make_team_private_description": "Po zapnutí této funkce neuvidí členové vašeho týmu ostatní členy týmu.",
"you_cannot_see_team_members": "V soukromém týmu nelze vidět všechny členy týmu.",
"allow_booker_to_select_duration": "Umožnit rezervující osobě vybrat dobu trvání",
"impersonate_user_tip": "Všechna použití této funkce jsou kontrolována.",
@@ -1327,7 +1327,7 @@
"download_responses_description": "Stáhněte si všechny odpovědi do formuláře ve formátu CSV.",
"download": "Stáhnout",
"download_recording": "Stáhnout nahrávku",
"recording_from_your_recent_call": "Nahrávka z vašeho nedávného hovoru v {{appName}} je připravena ke stažení.",
"recording_from_your_recent_call": "Nahrávka z vašeho nedávného hovoru v {{appName}} je připravena ke stažení",
"create_your_first_form": "Vytvořte svůj první formulář",
"create_your_first_form_description": "Pomocí adresných formulářů můžete klást kvalifikační otázky a adresovat je správné osobě nebo typu události.",
"create_your_first_webhook": "Vytvořte svůj první webhook",
@@ -1659,10 +1659,10 @@
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_user_cta": "Zobrazit pozvánku",
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{{entity}} v aplikaci {{appName}}",
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{entity}} v aplikaci {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu ({{entity}}) v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu ({{entity}}) umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem ({{entity}}) užívat plánování bez stresu.",
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu {{entity}} v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu {{entity}} umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem {{entity}} užívat plánování bez stresu.",
"email_no_user_step_one": "Vyberte si uživatelské jméno",
"email_no_user_step_two": "Připojte svůj účet kalendáře",
"email_no_user_step_three": "Nastavte si dostupnost",
@@ -1740,7 +1740,7 @@
"no_responses_yet": "Zatím žádné odpovědi",
"no_routes_defined": "Není definováno žádné směrování",
"this_will_be_the_placeholder": "Toto bude zástupný znak",
"error_booking_event": "Při rezervaci události došlo k chybě, obnovte prosím stránku a zkuste to znovu.",
"error_booking_event": "Při rezervaci události došlo k chybě, obnovte prosím stránku a zkuste to znovu",
"timeslot_missing_title": "Nebyl vybrán žádný časový slot",
"timeslot_missing_description": "Vyberte časový slot, aby mohla být událost zaregistrována.",
"timeslot_missing_cta": "Vybrat časový slot",
@@ -1802,7 +1802,7 @@
"least_booked_members": "Nejméně rezervovaných členů",
"events_created": "Události vytvořeny",
"events_completed": "Události dokončeny",
"events_cancelled": "Události zrušeny",
"events_cancelled": "Zrušené události",
"events_rescheduled": "Události přesunuty",
"from_last_period": "z posledního období",
"from_to_date_period": "Od: {{startDate}} do: {{endDate}}",
@@ -1838,9 +1838,9 @@
"confirm_your_details": "Potvrďte své údaje",
"copy_invite_link": "Zkopírovat odkaz pozvánky",
"edit_invite_link": "Upravit nastavení odkazu",
"invite_link_copied": "Odkaz pozvánky zkopírován",
"invite_link_deleted": "Odkaz pozvánky odstraněn",
"invite_link_updated": "Nastavení odkazu pozvánky uloženo",
"invite_link_copied": "Odkaz pozvánky byl zkopírován",
"invite_link_deleted": "Odkaz pozvánky byl odstraněn",
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
"link_expires_after": "Platnost odkazů vyprší za...",
"one_day": "1 den",
"seven_days": "7 dní",
@@ -1856,7 +1856,7 @@
"email_invite_team_bulk": "Počet pozvaných uživatelů: {{userCount}}",
"error_collecting_card": "Chyba při výběru karty",
"image_size_limit_exceed": "Velikost nahraného obrázku by neměla překročit limit 5 MB",
"unauthorized_workflow_error_message": "{{errorCode}}: Nejste oprávněni zapnout nebo vypnout tento pracovní postup.",
"unauthorized_workflow_error_message": "{{errorCode}}: Nejste oprávněni zapnout nebo vypnout tento pracovní postup",
"inline_embed": "Vložit na řádek",
"load_inline_content": "Načte váš typ události přímo do řádků s ostatním obsahem webu.",
"floating_pop_up_button": "Plovoucí překryvné tlačítko",
@@ -1895,10 +1895,10 @@
"about_your_organization": "Informace o vaší organizaci",
"about_your_organization_description": "Organizace jsou sdílená prostředí, ve kterých můžete vytvořit několik týmů se sdílenými členy, typy událostí, aplikacemi, pracovními postupy a mnoha dalšími věcmi.",
"create_your_teams": "Vytvoření týmů",
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování.",
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování",
"invite_organization_admins": "Pozvěte správce své organizace",
"invite_organization_admins_description": "Tito správci budou mít přístup ke všem týmům ve vaší organizaci. Správce a členy týmů můžete přidat později.",
"set_a_password": "Nastavte heslo",
"set_a_password": "Nastavte si heslo",
"set_a_password_description": "Tím se vytvoří nový uživatelský účet s e-mailem vaší organizace a tímto heslem.",
"organization_logo": "Logo organizace",
"organization_about_description": "Několik vět o vaší organizaci. Objeví se na veřejné stránce profilu vaší organizace.",
@@ -1923,7 +1923,7 @@
"my_settings": "Moje nastavení",
"crm": "CRM",
"messaging": "Zasílání zpráv",
"sender_id_info": "Jméno nebo číslo zobrazené jako odesílatel SMS (některé země nepovolují alfanumerické ID odesílatele).",
"sender_id_info": "Jméno nebo číslo zobrazené jako odesílatel SMS (některé země nepovolují alfanumerické ID odesílatele)",
"org_admins_can_create_new_teams": "Nové týmy může vytvářet pouze správce vaší organizace",
"google_new_spam_policy": "Nové zásady společnosti Google týkající se nevyžádané pošty by mohly zabránit tomu, abyste dostávali e-mailová oznámení a oznámení kalendáře o této rezervaci.",
"resolve": "Vyřešit",
+67 -61
View File
@@ -13,8 +13,8 @@
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"verify_email_subject": "{{appName}}: Bestätigen Sie Ihr Konto",
"check_your_email": "Schauen Sie in Ihrem E-Mail-Postfach nach",
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die beste Zustellbarkeit für E-Mails und Kalender von {{appName}} zu gewährleisten.",
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die beste Zustellbarkeit von E-Mails und Kalenderdaten zu gewährleisten",
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die Zustellbarkeit für E-Mails und Kalenderbenachrichtigungen von {{appName}} zu gewährleisten.",
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
"verify_email_email_button": "E-Mail bestätigen",
"copy_somewhere_safe": "Speichern Sie diesen API-Schlüssel an einem sicheren Ort. Sie können ihn nicht erneut ansehen.",
@@ -137,11 +137,11 @@
"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_upgraded_successfully": "Das Update für Ihre Organisation war erfolgreich!",
"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,",
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}} Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}}-Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
"confirm_delete_webhook": "Ja, Webhook löschen",
"edit_webhook": "Webhook bearbeiten",
"delete_webhook": "Webhook löschen",
@@ -260,7 +260,7 @@
"organization_general_description": "Einstellungen für die Sprache und Zeitzone Ihres Teams verwalten",
"few_sentences_about_yourself": "Schreiben Sie eine kurze Beschreibung, welche auf Ihrer persönlichen URL-Seite erscheinen wird.",
"nearly_there": "Fast fertig",
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann besser wissen, wen sie buchen.",
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann genauer wissen, bei wem sie einen Termin buchen.",
"set_availability_instructions": "Definieren Sie Zeiträume, wenn Sie wiederkehrend verfügbar sind. Sie können später weitere Zeiträume erstellen und verschiedenen Kalendern zuordnen.",
"set_availability": "Verfügbarkeit festlegen",
"continue_without_calendar": "Ohne Kalender fortfahren",
@@ -318,11 +318,11 @@
"password_has_been_reset_login": "Ihr Passwort wurde zurückgesetzt. Sie können sich jetzt mit Ihrem neu erstellten Passwort anmelden.",
"layout": "Layout",
"bookerlayout_default_title": "Standardansicht",
"bookerlayout_description": "Sie können mehrere auswählen und Ihre Bucher können die Ansicht wechseln.",
"bookerlayout_description": "Sie können mehrere auswählen und Ihre Bucher können anschließend die Ansicht wechseln.",
"bookerlayout_user_settings_title": "Buchungslayout",
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können anschließend die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
"bookerlayout_month_view": "Monat",
"bookerlayout_week_view": "Woche",
"bookerlayout_week_view": "Wöchentlich",
"bookerlayout_column_view": "Spalte",
"bookerlayout_error_min_one_enabled": "Es muss mindestens ein Layout aktiviert sein.",
"bookerlayout_error_default_not_enabled": "Das Layout, das Sie als Standardansicht ausgewählt haben, ist nicht Teil der aktivierten Layouts.",
@@ -404,6 +404,8 @@
"allow_dynamic_booking_tooltip": "Gruppenbuchungslinks, die dynamisch durch Hinzufügen mehrerer Benutzernamen mit einem '+' erstellt werden können. Beispiel: '{{appName}}/bailey+peer'",
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
"dynamic_booking": "Dynamische Gruppenlinks",
"allow_seo_indexing": "Suchmaschinen erlauben, auf Ihre öffentlichen Inhalte zuzugreifen",
"seo_indexing": "Suchmaschinen-Indexierung erlauben",
"email": "E-Mail",
"email_placeholder": "max@example.com",
"full_name": "Vollständiger Name",
@@ -426,8 +428,8 @@
"team_webhooks": "Team-Webhooks",
"create_new_webhook_to_account": "Erstellen Sie einen neuen Webhook in Ihrem Account",
"new_webhook": "Neuer Webhook",
"receive_cal_meeting_data": "Erhalten Sie {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn ein Termin geplant oder abgesagt wird.",
"receive_cal_event_meeting_data": "Erhalten Sie {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn dieser Termin geplant oder abgesagt wird.",
"receive_cal_meeting_data": "Erhalten Sie in Echtzeit {{appName}}-Termindaten unter einer bestimmten URL, wenn ein Termin geplant oder abgesagt wird.",
"receive_cal_event_meeting_data": "Erhalten Sie in Echtzeit {{appName}}-Termindaten unter einer bestimmten URL, wenn dieser Termin geplant oder abgesagt wird.",
"responsive_fullscreen_iframe": "Responsives Vollbild-iFrame",
"loading": "Wird geladen...",
"deleting": "Lösche...",
@@ -565,7 +567,7 @@
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
"members": "Mitglieder",
"organization_members": "Organisationsmitglieder",
"organization_members": "Mitglieder der Organization",
"member": "Mitglied",
"number_member_one": "{{count}} Mitglied",
"number_member_other": "{{count}} Mitglieder",
@@ -600,7 +602,7 @@
"confirm_all": "Alle bestätigen",
"disband_team": "Team auflösen",
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
"disband_org": "Organisation auflösen",
"disband_org": "Organization auflösen",
"disband_org_confirmation_message": "Sind Sie sicher, dass Sie diese Organisation auflösen möchten? Alle Teams und Mitglieder werden gelöscht.",
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
"confirm_disband_team": "Ja, Team auflösen",
@@ -713,7 +715,7 @@
"create_team_to_get_started": "Erstellen Sie ein Team um loszulegen",
"teams": "Teams",
"team": "Team",
"organization": "Organisation",
"organization": "Organization",
"team_billing": "Abrechnung der Teams",
"team_billing_description": "Verwalte die Abrechungen deines Teams",
"upgrade_to_flexible_pro_title": "Wir haben die Abrechnung für Teams geändert",
@@ -924,7 +926,7 @@
"create_events_on": "Erstelle Termine in:",
"enterprise_license": "Das ist eine Enterprise-Funktion",
"enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.",
"enterprise_license_development": "Sie können diese Funktion im Entwicklungsmodus testen. Zur Nutzung im Produktionsmodus, besuchen Sie bitte <2>/auth/setup</2>, um einen Lizenzschlüssel einzugeben.",
"enterprise_license_development": "Sie können diese Funktion im Entwicklungsmodus testen. Zur Nutzung im regulären Arbeitsumfeld, besuchen Sie bitte <2>/auth/setup</2>, um einen Lizenzschlüssel einzugeben.",
"missing_license": "Lizenz fehlt",
"signup_requires": "Kommerzielle Lizenz erforderlich",
"signup_requires_description": "{{companyName}} bietet zur Zeit keine kostenlose Open-Source-Version der Anmeldeseite an. Um vollen Zugriff auf die Registrierungskomponenten zu erhalten, müssen Sie eine kommerzielle Lizenz erwerben. Für den persönlichen Gebrauch empfehlen wir die Prisma Data Platform oder jede andere Postgres-Schnittstelle zur Erstellung von Accounts.",
@@ -1018,7 +1020,7 @@
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
"make_team_private": "Team privat stellen",
"make_team_private_description": "Deine Teammitglieder können keine anderen Teammitglieder sehen, wenn dies aktiviert ist.",
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
"allow_booker_to_select_duration": "Bucher erlauben, die Länge zu wählen",
"impersonate_user_tip": "Alle Verwendungen dieser Funktion werden geprüft.",
@@ -1351,9 +1353,9 @@
"event_one": "Ereignis",
"event_other": "Ereignisse",
"profile_team_description": "Einstellungen für Ihr Teamprofil verwalten",
"profile_org_description": "Einstellungen für Ihr Organisationsprofil verwalten",
"profile_org_description": "Einstellungen für Ihr Organization-Profil verwalten",
"members_team_description": "Benutzer in der Gruppe",
"organization_description": "Administratoren und Mitglieder in Ihrer Organisation verwalten",
"organization_description": "Administratoren und Mitglieder in Ihrer Organization verwalten",
"team_url": "Team-URL",
"team_members": "Teammitglieder",
"more": "Mehr",
@@ -1499,7 +1501,7 @@
"yearly": "Jährlich",
"checkout": "Kasse",
"your_team_disbanded_successfully": "Ihr Team wurde erfolgreich aufgelöst",
"your_org_disbanded_successfully": "Ihre Organisation wurde erfolgreich aufgelöst",
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
"error_creating_team": "Fehler beim Erstellen des Teams",
"you": "Sie",
"resend_email": "E-Mail erneut senden",
@@ -1511,7 +1513,7 @@
"url_taken": "Diese URL ist bereits vergeben",
"problem_registering_domain": "Es gab ein Problem bei der Registrierung der Subdomain, bitte versuchen Sie es erneut oder kontaktieren Sie einen Administrator",
"team_publish": "Team veröffentlichen",
"number_text_notifications": "Telefonnummer (Text-Benachrichtigungen)",
"number_text_notifications": "Telefonnummer (Textbenachrichtigungen)",
"attendee_email_variable": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
@@ -1534,8 +1536,8 @@
"find_the_best_person": "Finden Sie die beste verfügbare Person und rotieren Sie durch Ihr Team.",
"fixed_round_robin": "Festgelegter Round Robin",
"add_one_fixed_attendee": "Fügen Sie einen festen Teilnehmer und einen Round Robin mit einer Reihe von Teilnehmern hinzu.",
"calcom_is_better_with_team": "{{appName}} ist mit Teams besser",
"the_calcom_team": "Das {{companyName}} Team",
"calcom_is_better_with_team": "{{appName}} ist mit Teams noch besser",
"the_calcom_team": "Das {{companyName}}-Team",
"add_your_team_members": "Fügen Sie Ihre Teammitglieder zu Ihren Termintypen hinzu. Nutzen Sie kollektive Planung, um alle einzubinden oder finden Sie die geeignetste Person mit der Round Robin Planung.",
"booking_limit_reached": "Das Buchungslimit für diesen Termintyp wurde erreicht",
"duration_limit_reached": "Das Zeitlimit für diesen Termintyp wurde erreicht",
@@ -1688,7 +1690,7 @@
"scheduler": "{Scheduler}",
"no_workflows": "Keine Workflows",
"change_filter": "Ändern Sie den Filter, um Ihre eigenen und Ihre Team-Workflows anzuzeigen.",
"change_filter_common": "Ändern Sie Ihre Filteroptionen um die Ergebnisse zu sehen.",
"change_filter_common": "Passen Sie Ihre Filteroptionen an, um die Ergebnisse zu sehen.",
"no_results_for_filter": "Keine Ergebnisse für diesen Filter",
"recommended_next_steps": "Empfohlene nächste Schritte",
"create_a_managed_event": "Einen verwalteten Ereignistyp erstellen",
@@ -1700,7 +1702,7 @@
"no_longer_attending": "Sie nehmen an diesem Termin nicht mehr teil",
"attendee_no_longer_attending_subject": "Ein Teilnehmer nimmt nicht mehr an {{title}} am {{date}} teil",
"attendee_no_longer_attending": "Ein Teilnehmer nimmt nicht mehr an Ihrem Termin teil",
"attendee_no_longer_attending_subtitle": "{{name}} hat abgesagt. Das bedeutet, dass ein Platz für dieses Zeitfenster geöffnet wurde",
"attendee_no_longer_attending_subtitle": "{{name}} hat abgesagt. Dies bedeutet, dass ein Platz in diesem Zeitfenster frei wurde",
"create_event_on": "Erstelle Termin am",
"create_routing_form_on": "Weiterleitungsformular erstellen am",
"default_app_link_title": "Standard-App-Link festlegen",
@@ -1753,7 +1755,7 @@
"spot_popular_event_types": "Machen Sie beliebte Ereignistypen ausfindig",
"spot_popular_event_types_description": "Sehen Sie, welche Ihrer Ereignistypen die meisten Klicks und Buchungen erhalten",
"no_responses_yet": "Noch keine Antworten",
"no_routes_defined": "Keine Routen definiert",
"no_routes_defined": "Keine Weiterleitungsrouten definiert",
"this_will_be_the_placeholder": "Dies wird der Platzhalter sein",
"error_booking_event": "Bei der Buchung dieses Termins ist ein Fehler aufgetreten, bitte aktualisieren Sie die Seite und versuchen Sie es erneut",
"timeslot_missing_title": "Kein Zeitfenster ausgewählt",
@@ -1896,68 +1898,69 @@
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
"install_app_on": "App installieren auf",
"create_for": "Erstellen für",
"setup_organization": "Eine Organisation einrichten",
"organization_banner_description": "Schaffen Sie eine Umgebung, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
"organization_banner_title": "Verwalten Sie Organisationen mit mehreren Teams",
"set_up_your_organization": "Ihre Organisation einrichten",
"organizations_description": "Organisationen sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
"must_enter_organization_name": "Es muss ein Organisationsname eingegeben werden",
"must_enter_organization_admin_email": "Sie müssen Ihre Organisations-Mailadresse eingeben",
"admin_email": "Ihre Organisations-E-Mail-Adresse",
"currency": "Währung",
"setup_organization": "Eine Organization einrichten",
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
"set_up_your_organization": "Ihre Organization einrichten",
"organizations_description": "Organizations sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
"must_enter_organization_name": "Es muss ein Name für die Organization eingegeben werden",
"must_enter_organization_admin_email": "Sie müssen Ihre Organization-Mailadresse eingeben\n",
"admin_email": "Ihre Organization-E-Mail-Adresse",
"admin_username": "Benutzername des Administrators",
"organization_name": "Name der Organisation",
"organization_url": "URL der Organisation",
"organization_verify_header": "Bestätigen Sie Ihre Organisations-Mailadresse",
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organisation fortzusetzen.",
"organization_name": "Name der Organization",
"organization_url": "URL der Organization ",
"organization_verify_header": "Bestätigen Sie Ihre Organization-Mailadresse",
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organization fortzusetzen.",
"additional_url_parameters": "Zusätzliche URL-Parameter",
"about_your_organization": "Über Ihre Organisation",
"about_your_organization_description": "Organisationen sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.",
"about_your_organization": "Über Ihre Organization",
"about_your_organization_description": "Organizations sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.\n",
"create_your_teams": "Erstellen Sie Ihre Teams",
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organisation hinzufügen",
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organisation ein",
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organisation. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organization hinzufügen",
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organization ein",
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organization. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
"set_a_password": "Legen Sie ein Passwort fest",
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organisations-Mailadresse und diesem Passwort erstellen.",
"organization_logo": "Logo der Organisation",
"organization_about_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der öffentlichen Profilseite Ihrer Organisation erscheinen.",
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organization-Mailadresse und diesem Passwort erstellen.",
"organization_logo": "Logo der Organization",
"organization_about_description": "Ein paar Sätze zu Ihrer Organization. Dies wird auf der öffentlichen Profilseite Ihrer Organization erscheinen.",
"ill_do_this_later": "Ich werde das später tun",
"verify_your_email": "Bestätigen Sie Ihre E-Mail",
"verify_your_email": "Bestätigen Sie Ihre E-Mail-Adresse",
"enter_digit_code": "Geben Sie den 6-stelligen Code ein, den wir an {{email}} gesendet haben",
"verify_email_organization": "Bestätigen Sie Ihre E-Mail, um eine Organisation zu erstellen",
"verify_email_organization": "Bestätigen Sie Ihre E-Mail-Adresse, um eine Organization zu erstellen",
"code_provided_invalid": "Der eingegebene Code ist ungültig, versuchen Sie es erneut",
"email_already_used": "Die E-Mail-Adresse wird bereits verwendet",
"organization_admin_invited_heading": "Sie wurden eingeladen, ein Mitglied von {{orgName}} zu werden",
"organization_admin_invited_body": "Schließen Sie sich Ihrem Team bei {{orgName}} an und konzentrieren Sie sich auf Ihre Meetings, nicht auf das Planen von Meetings!",
"duplicated_slugs_warning": "Die folgenden Teams konnten aufgrund von doppelten Slugs nicht erstellt werden: {{slugs}}",
"team_names_empty": "Teamnamen dürfen nicht leer sein",
"team_names_repeated": "Teamnamen dürfen nicht wiederholt vorkommen",
"user_belongs_organization": "Der Benutzer gehört einer Organisation an",
"org_no_teams_yet": "Diese Organisation hat noch keine Teams",
"team_names_repeated": "Teamnamen dürfen nicht bereits verwendet werden",
"user_belongs_organization": "Der Benutzer gehört einer Organization an",
"org_no_teams_yet": "Diese Organization hat noch keine Teams",
"org_no_teams_yet_description": "Falls Sie ein Administrator sind, sollten Sie unbedingt Teams erstellen, welche hier angezeigt werden können.",
"set_up": "Einrichten",
"set_up_your_profile": "Richten Sie Ihr Profil ein",
"set_up_your_profile_description": "Lassen Sie Leute wissen, wer Sie innerhalb von {{orgName}} sind und wann sie mit Ihrem öffentlichen Link arbeiten.",
"set_up_your_profile_description": "Lassen Sie Leute beim Klicken auf Ihren öffentlichen Link wissen, welche Funktion Sie innerhalb von {{orgName}} haben.",
"my_profile": "Mein Profil",
"my_settings": "Meine Einstellungen",
"crm": "CRM",
"messaging": "Nachrichten",
"sender_id_info": "Name oder Nummer, welcher bzw. welche als Absender einer SMS angezeigt wird (einige Länder erlauben keine alphanumerischen Absender-IDs)",
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organisation kann neue Teams erstellen",
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organization kann neue Teams erstellen",
"google_new_spam_policy": "Die neuen Spam-Richtlinien von Google könnten verhindern, dass Sie E-Mail- und Kalenderbenachrichtigungen zu dieser Buchung erhalten.",
"resolve": "Beheben",
"no_organization_slug": "Beim Erstellen von Teams für diese Organisation ist ein Fehler aufgetreten. URL Slug fehlt.",
"org_name": "Name der Organisation",
"org_url": "URL der Organisation",
"copy_link_org": "Link zur Organisation kopieren",
"404_the_org": "Die Organisation",
"resolve": "Problem lösen",
"no_organization_slug": "Beim Erstellen von Teams für diese Organization ist ein Fehler aufgetreten. URL-Slug fehlt.",
"org_name": "Name der Organization",
"org_url": "URL der Organization",
"copy_link_org": "Link zur Organization kopieren",
"404_the_org": "Die Organization",
"404_the_team": "Das Team",
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organisation",
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organization",
"404_claim_entity_team": "Dieses Team beanspruchen und gemeinsam mit der Verwaltung von Verfügbarkeitsplänen beginnen",
"insights_all_org_filter": "Alle Apps",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Benutzer: {{userName}}",
"insights_subtitle": "Sehen Sie sich Buchungs-Insights über Ihre Termine an",
"custom_plan": "Benutzerdefinierter Plan",
"insights_subtitle": "Sehen Sie sich Buchungs-Insights zu Ihren Termine an",
"custom_plan": "Maßgeschneiderter Tarif",
"email_embed": "E-Mail Einbettung",
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
"select_time": "Zeit auswählen",
@@ -1971,10 +1974,13 @@
"org_team_names_example_5": "z.B. Data Analytics Team",
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
"kyc_verification_information": "Aus Sicherheitsgründen müssen Sie Ihren {{teamOrAccount}} verifizieren, bevor Sie Textnachrichten an die Teilnehmer senden können. Bitte kontaktieren Sie uns unter <a>{{supportEmail}}</a> und geben Sie folgende Informationen an:",
"org_admin_other_teams": "Weitere Teams",
"no_other_teams_found": "Keine weiteren Teams gefunden",
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
"attendee_first_name_variable": "Vorname des Teilnehmers",
"attendee_last_name_variable": "Nachname des Teilnehmers",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Zeichenketten über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"my_availability": "Meine Verfügbarkeit",
"team_availability": "Teamverfügbarkeit",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+31 -4
View File
@@ -407,6 +407,8 @@
"allow_dynamic_booking_tooltip": "Group booking links that can be created dynamically by adding multiple usernames with a '+'. example: '{{appName}}/bailey+peer'",
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
"dynamic_booking": "Dynamic group links",
"allow_seo_indexing": "Allow search engines to access your public content",
"seo_indexing": "Allow SEO Indexing",
"email": "Email",
"email_placeholder": "jdoe@example.com",
"full_name": "Full name",
@@ -560,6 +562,7 @@
"leave": "Leave",
"profile": "Profile",
"my_team_url": "My team URL",
"my_teams": "My teams",
"team_name": "Team Name",
"your_team_name": "Your team name",
"team_updated_successfully": "Team updated successfully",
@@ -806,7 +809,6 @@
"add_new_event_type": "Add a new event type",
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
"length": "Length",
"people":"People",
"minimum_booking_notice": "Minimum Notice",
"offset_toggle": "Offset start times",
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
@@ -870,6 +872,7 @@
"account_managed_by_identity_provider_description": "To change your email, password, enable two-factor authentication and more, please visit your {{provider}} account settings.",
"signin_with_google": "Sign in with Google",
"signin_with_saml": "Sign in with SAML",
"signin_with_saml_oidc": "Sign in with SAML/OIDC",
"you_will_need_to_generate": "You will need to generate an access token from your old scheduling tool.",
"import": "Import",
"import_from": "Import from",
@@ -927,7 +930,7 @@
"verify_wallet": "Verify Wallet",
"create_events_on": "Create events on",
"enterprise_license": "This is an enterprise feature",
"enterprise_license_description": "To enable this feature, have an administrator go to {{setupUrl}} to enter a license key. If a license key is already in place, please contact {{supportMail}} for help.",
"enterprise_license_description": "To enable this feature, have an administrator go to <2>/auth/setup</2> to enter a license key. If a license key is already in place, please contact <5>{{SUPPORT_MAIL_ADDRESS}}</5> for help.",
"enterprise_license_development": "You can test this feature on development mode. For production usage please have an administrator go to <2>/auth/setup</2> to enter a license key.",
"missing_license": "Missing License",
"signup_requires": "Commercial license required",
@@ -1263,6 +1266,7 @@
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
"user_has_no_bio": "This user has not added a bio yet.",
"delete_account_modal_title": "Delete Account",
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
"delete_my_account": "Delete my account",
@@ -1578,7 +1582,6 @@
"apps_settings": "Apps settings",
"fill_this_field": "Please fill in this field",
"options": "Options",
"no_options_available":"No options available",
"enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option",
"radio": "Radio",
@@ -1980,6 +1983,11 @@
"org_team_names_example_5": "e.g. Data Analytics Team",
"org_max_team_warnings": "You will be able to add more teams later on.",
"what_is_this_meeting_about": "What is this meeting about?",
"add_to_team":"Add to team",
"remove_users_from_org": "Remove users from organization",
"remove_users_from_org_confirm":"Are you sure you want to remove {{userCount}} users from this organization?",
"user_has_no_schedules":"This user has not setup any schedules yet",
"user_isnt_in_any_teams":"This user is not in any teams",
"requires_booker_email_verification": "Requires booker email verification",
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
@@ -1990,7 +1998,7 @@
"kyc_verification": "KYC Verification",
"organizations": "Organizations",
"org_admin_other_teams": "Other teams",
"org_admin_other_teams_description": "Here you can see teams inside your organization but that you are not part of. You can add yourself to them if needed.",
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
"no_other_teams_found": "No other teams found",
"no_other_teams_found_description": "There are no other teams in this organization.",
"attendee_first_name_variable": "Attendee first name",
@@ -1999,5 +2007,24 @@
"attendee_last_name_info": "The person booking's last name",
"me": "Me",
"verify_team_tooltip": "Verify your team to enable sending messages to attendees",
"member_removed": "Member removed",
"my_availability": "My Availability",
"team_availability": "Team Availability",
"admin_org_notification_email_subject": "New organization created: pending action",
"hi_admin": "Hi Administrator",
"admin_org_notification_email_title": "An organization requires DNS setup",
"admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.<br /><br />Please be sure to configure your DNS registry to point the subdomain corresponding to the new organization to where the main app is running. Otherwise the organization will not work.<br /><br />Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.<br /><br />You can do it either with the A Record:",
"admin_org_notification_email_body_part2": "Or the CNAME record:",
"admin_org_notification_email_body_part3": "Once you configure the subdomain, please mark the DNS configuration as done in Organizations Admin Settings.",
"admin_org_notification_email_cta": "Go to Organizations Admin Settings",
"org_has_been_processed": "Org has been processed",
"org_error_processing": "There has been an error processing this organization",
"orgs_page_description": "A list of all organizations. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation.",
"unverified": "Unverified",
"dns_missing": "DNS Missing",
"mark_dns_configured": "Mark as DNS configured",
"value": "Value",
"your_organization_updated_sucessfully": "Your organization updated successfully",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+10 -10
View File
@@ -11,14 +11,14 @@
"calcom_explained_new_user": "¡Termine de configurar su cuenta de {{appName}}! Solo le faltan unos pasos para resolver todos sus problemas de programación.",
"have_any_questions": "¿Tienes preguntas? Estamos aquí para ayudar.",
"reset_password_subject": "{{appName}}: Instrucciones para restablecer la contraseña",
"verify_email_subject": "{{appName}}: verifique su cuenta",
"verify_email_subject": "{{appName}}: Verifique su cuenta",
"check_your_email": "Revise su correo electrónico",
"verify_email_page_body": "Enviamos un correo electrónico a {{email}}. Es importante que verifique su dirección de correo electrónico para garantizar que {{appName}} le ofrezca el mejor servicio de entrega de correos y calendarios.",
"verify_email_banner_body": "Verifique su dirección de correo electrónico para garantizar la mejor entrega de correo electrónico y calendario",
"verify_email_email_header": "Verifique su dirección de correo electrónico",
"verify_email_email_button": "Verificar correo electrónico",
"verify_email_email_body": "Verifique su dirección de correo electrónico al hacer clic en el botón a continuación.",
"verify_email_email_link_text": "Este es el enlace en caso de que no le guste hacer clic en los botones:",
"verify_email_email_link_text": "Este es el enlace en caso de que no prefiera hacer clic en los botones:",
"email_sent": "Correo electrónico enviado correctamente",
"event_declined_subject": "Rechazado: {{title}} en {{date}}",
"event_cancelled_subject": "Cancelado: {{title}} el {{date}}",
@@ -130,7 +130,7 @@
"upgrade_banner_action": "Actualizar aquí",
"team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!",
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
"org_upgraded_successfully": "Su organización se actualizó con éxito.",
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
"hey_there": "Hola,",
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
@@ -248,7 +248,7 @@
"organization_general_description": "Administre los ajustes para el idioma y la zona horaria de su equipo",
"few_sentences_about_yourself": "Unas pocas frases sobre ti mismo. Esto aparecerá en tu página de url personal.",
"nearly_there": "¡Ya casi está!",
"nearly_there_instructions": "Lo último, una breve descripción sobre usted y una foto realmente lo ayudan a obtener reservas y a que las personas sepan con quién están reservando.",
"nearly_there_instructions": "Lo último, una breve descripción sobre usted y una foto lo ayudan a obtener reservas y a que las personas sepan con quién están reservando.",
"set_availability_instructions": "Defina los intervalos de tiempo cuando esté disponible de forma recurrente. Puede crear más tarde y asignarlos a diferentes calendarios.",
"set_availability": "Establecer Disponibilidad",
"continue_without_calendar": "Continuar sin calendario",
@@ -306,9 +306,9 @@
"password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.",
"layout": "Diseño",
"bookerlayout_default_title": "Vista predeterminada",
"bookerlayout_description": "Puede seleccionar varios y sus reservadores pueden cambiar de vista.",
"bookerlayout_description": "Puede seleccionar varios y quienes le reserven pueden cambiar de vista.",
"bookerlayout_user_settings_title": "Diseño de reserva",
"bookerlayout_user_settings_description": "Puede seleccionar varios y los reservadores pueden cambiar de vista. Esto se puede anular por evento.",
"bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
"bookerlayout_month_view": "Mes",
"bookerlayout_week_view": "Semanal",
"bookerlayout_column_view": "Columna",
@@ -1327,7 +1327,7 @@
"download_responses_description": "Descargue todas las respuestas de su formulario en formato CSV.",
"download": "Descargar",
"download_recording": "Descargar grabación",
"recording_from_your_recent_call": "Una grabación de su llamada reciente en {{appName}} está lista para descargarla",
"recording_from_your_recent_call": "Una grabación de su llamada reciente en {{appName}} está lista para descargar",
"create_your_first_form": "Crea tu primer formulario",
"create_your_first_form_description": "Con los formularios de orientación puedes hacer preguntas calificadoras y encaminarte hacia la persona o tipo de evento correctos.",
"create_your_first_webhook": "Crea tu primer Webhook",
@@ -1661,7 +1661,7 @@
"email_user_cta": "Ver la invitación",
"email_no_user_invite_heading": "Lo invitaron a unirse a {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin correos electrónicos de ida y vuelta.",
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin necesidad de correos electrónicos.",
"email_no_user_invite_steps_intro": "Lo guiaremos a través de unos pocos pasos y disfrutará de una programación sin estrés con su {{entity}} en muy poco tiempo.",
"email_no_user_step_one": "Elija su nombre de usuario",
"email_no_user_step_two": "Conecte su cuenta de calendario",
@@ -1687,7 +1687,7 @@
"attendee_no_longer_attending": "Un asistente ya no asistirá a su evento",
"attendee_no_longer_attending_subtitle": "{{name}} ha cancelado. Esto significa que está disponible una plaza para esta franja horaria",
"create_event_on": "Crear evento en",
"create_routing_form_on": "Crear formulario de enrutamiento el",
"create_routing_form_on": "Crear formulario de enrutamiento en",
"default_app_link_title": "Establecer un enlace de aplicación predeterminado",
"default_app_link_description": "Establecer un enlace de aplicación predeterminado permite que todos los tipos de eventos recién creados utilicen el enlace de aplicación que establezca.",
"organizer_default_conferencing_app": "Aplicación por defecto del organizador",
@@ -1934,7 +1934,7 @@
"404_the_org": "La organización",
"404_the_team": "El equipo",
"404_claim_entity_org": "Reclame su subdominio para su organización",
"404_claim_entity_team": "Reclama este equipo y empiece a gestionar los horarios de forma colectiva",
"404_claim_entity_team": "Reclame este equipo y empiece a gestionar los horarios de forma colectiva",
"insights_all_org_filter": "Todas las aplicaciones",
"insights_team_filter": "Equipo: {{teamName}}",
"insights_user_filter": "Usuario: {{userName}}",
+27 -2
View File
@@ -407,6 +407,8 @@
"allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».",
"allow_dynamic_booking": "Autoriser les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques",
"dynamic_booking": "Liens de groupe dynamiques",
"allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.",
"seo_indexing": "Autoriser l'indexation SEO",
"email": "E-mail",
"email_placeholder": "jdoe@exemple.com",
"full_name": "Nom complet",
@@ -560,6 +562,7 @@
"leave": "Quitter",
"profile": "Profil",
"my_team_url": "Lien de mon équipe",
"my_teams": "Mes équipes",
"team_name": "Nom de l'équipe",
"your_team_name": "Nom de votre équipe",
"team_updated_successfully": "Équipe mise à jour avec succès",
@@ -607,7 +610,7 @@
"disband_org_confirmation_message": "Voulez-vous vraiment dissoudre cette organisation ? Toutes les équipes et tous les membres seront supprimés.",
"remove_member_confirmation_message": "Voulez-vous vraiment supprimer ce membre de l'équipe ?",
"confirm_disband_team": "Oui, dissoudre l'équipe",
"confirm_remove_member": "Oui, retirer le membre",
"confirm_remove_member": "Oui, supprimer le membre",
"remove_member": "Supprimer le membre",
"manage_your_team": "Gérez votre équipe",
"no_teams": "Vous n'avez pas encore d'équipe.",
@@ -869,6 +872,7 @@
"account_managed_by_identity_provider_description": "Pour modifier votre adresse e-mail et/ou votre mot de passe, activez l'authentification à deux facteurs et plus, veuillez consulter les paramètres de votre compte {{provider}}.",
"signin_with_google": "Se connecter avec Google",
"signin_with_saml": "Se connecter avec SAML",
"signin_with_saml_oidc": "Se connecter avec SAML/OIDC",
"you_will_need_to_generate": "Vous devrez générer un token d'accès à partir de votre ancien outil de planification.",
"import": "Importer",
"import_from": "Importer depuis",
@@ -926,7 +930,7 @@
"verify_wallet": "Vérifier le portefeuille",
"create_events_on": "Créer des événements dans :",
"enterprise_license": "Il s'agit d'une fonctionnalité d'entreprise",
"enterprise_license_description": "Pour activer cette fonctionnalité, demandez à un administrateur d'accéder à {{setupUrl}} pour saisir une clé de licence. Si une clé de licence est déjà en place, veuillez contacter {{supportMail}} pour obtenir de l'aide.",
"enterprise_license_description": "Pour activer cette fonctionnalité, demandez à un administrateur d'accéder à <2>/auth/setup</2> pour saisir une clé de licence. Si une clé de licence est déjà en place, veuillez contacter <5>{{SUPPORT_MAIL_ADDRESS}}</5> pour obtenir de l'aide.",
"enterprise_license_development": "Vous pouvez tester cette fonctionnalité en mode développement. Pour une utilisation en production, veuillez demander à un administrateur d'aller à <2>/auth/setup</2> pour entrer une clé de licence.",
"missing_license": "Licence manquante",
"signup_requires": "Licence commerciale requise",
@@ -1262,6 +1266,7 @@
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
"personal_cal_url": "Mon lien {{appName}} personnel",
"bio_hint": "Quelques mots à propos de vous. Ces informations apparaîtront sur votre page publique.",
"user_has_no_bio": "Cet utilisateur n'a pas encore ajouté de description.",
"delete_account_modal_title": "Supprimer le compte",
"confirm_delete_account_modal": "Voulez-vous vraiment supprimer votre compte {{appName}} ?",
"delete_my_account": "Supprimer mon compte",
@@ -1978,6 +1983,11 @@
"org_team_names_example_5": "p. ex. Équipe d'analyse de données",
"org_max_team_warnings": "Vous pourrez ajouter plus d'équipes plus tard.",
"what_is_this_meeting_about": "En quoi consiste ce rendez-vous ?",
"add_to_team": "Ajouter à l'équipe",
"remove_users_from_org": "Supprimer des utilisateurs de l'organisation",
"remove_users_from_org_confirm": "Voulez-vous vraiment supprimer {{userCount}} utilisateurs de cette organisation ?",
"user_has_no_schedules": "Cet utilisateur n'a pas encore configuré de planning",
"user_isnt_in_any_teams": "Cet utilisateur n'est dans aucune équipe",
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
"kyc_verification_information": "Pour garantir la sécurité, vous devez vérifier votre {{teamOrAccount}} avant d'envoyer des messages aux participants. Veuillez nous contacter à <a>{{supportEmail}}</a> et fournir les informations suivantes :",
"kyc_verification_documents": "<ul><li>Votre {{teamOrUser}}</li><li>Pour les entreprises : Joignez votre document de vérification d'entreprise</li><li>Pour les particuliers : Joignez une pièce d'identité</li></ul>",
@@ -1993,5 +2003,20 @@
"attendee_last_name_variable": "Nom du participant",
"me": "Moi",
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
"member_removed": "Membre supprimé",
"my_availability": "Mes disponibilités",
"team_availability": "Disponibilités de l'équipe",
"admin_org_notification_email_subject": "Nouvelle organisation créée : action en attente",
"admin_org_notification_email_title": "Une organisation nécessite une configuration DNS",
"admin_org_notification_email_body_part2": "Ou l'enregistrement CNAME :",
"admin_org_notification_email_body_part3": "Une fois le sous-domaine configuré, veuillez marquer la configuration DNS comme terminée dans les paramètres d'administration des organisations.",
"admin_org_notification_email_cta": "Accéder aux paramètres d'administration des organisations",
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
"unverified": "Non vérifié",
"dns_missing": "DNS manquant",
"mark_dns_configured": "Marquer comme DNS configuré",
"value": "Valeur",
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

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