From 19eced00f5568446cf2401e03f8b8cb8f554d1bd Mon Sep 17 00:00:00 2001 From: kremedev <134702704+kremedev@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:00:00 +0300 Subject: [PATCH 001/118] fix: Unable to modify the location of a booking when rescheduling (#11651) --- apps/web/pages/booking/[uid].tsx | 68 ++++++++++++++++--- .../BookEventForm/BookingFields.tsx | 7 ++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index f91e22c50f..af7b779dc8 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -115,6 +115,13 @@ export default function Success(props: SuccessProps) { const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone(); const location = props.bookingInfo.location as ReturnType; + let rescheduleLocation: string | undefined; + if ( + typeof props.bookingInfo.responses.location === "object" && + "optionValue" in props.bookingInfo.responses.location + ) { + rescheduleLocation = props.bookingInfo.responses.location.optionValue; + } const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse( props?.bookingInfo?.metadata || {} @@ -295,7 +302,14 @@ export default function Success(props: SuccessProps) { bookingInfo.status ); + const rescheduleLocationToDisplay = getSuccessPageLocationMessage( + rescheduleLocation ?? "", + t, + bookingInfo.status + ); + const providerName = guessEventLocationType(location)?.label; + const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label; return (
@@ -467,18 +481,50 @@ export default function Success(props: SuccessProps) { <>
{t("where")}
- {locationToDisplay.startsWith("http") ? ( - - {providerName || "Link"} - - + {!rescheduleLocation || locationToDisplay === rescheduleLocationToDisplay ? ( + locationToDisplay.startsWith("http") ? ( + + {providerName || "Link"} + + + ) : ( + locationToDisplay + ) ) : ( - locationToDisplay + <> + {!!formerTime && + (locationToDisplay.startsWith("http") ? ( + + {providerName || "Link"} + + + ) : ( +

{locationToDisplay}

+ ))} + {rescheduleLocationToDisplay.startsWith("http") ? ( + + {rescheduleProviderName || "Link"} + + + ) : ( + rescheduleLocationToDisplay + )} + )}
diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx index ab1505a787..a10cdae5b1 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -77,6 +77,13 @@ export const BookingFields = ({ return null; } + // Attendee location field can be edited during reschedule + if (field.name === SystemField.Enum.location) { + if (locationResponse?.value === "attendeeInPerson" || "phone") { + readOnly = false; + } + } + // Dynamically populate location field options if (field.name === SystemField.Enum.location && field.type === "radioInput") { if (!field.optionsInputs) { From be1517facd982e017e52679afe2524b50c7bdf98 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:42:49 +0100 Subject: [PATCH 002/118] fix: get correct count for team members in slider (#12017) --- .../availability/team/listTeamAvailability.handler.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts b/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts index f495e8d047..3213573854 100644 --- a/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts @@ -1,3 +1,5 @@ +import { Prisma } from "@prisma/client"; + import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import type { DateRange } from "@calcom/lib/date-ranges"; @@ -120,9 +122,16 @@ async function getInfoForAllTeams({ ctx, input }: GetOptions) { limit, }); + // Get total team count across all teams the user is in (for pagination) + + const totalTeamMembers = + await prisma.$queryRaw`SELECT COUNT(DISTINCT "userId")::integer from "Membership" WHERE "teamId" IN (${Prisma.join( + teamIds + )})`; + return { teamMembers, - totalTeamMembers: teamMembers.length, + totalTeamMembers, }; } From d043de77249a2155bfaf983fe135b030ce6895ff Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:02:08 +0100 Subject: [PATCH 003/118] fix: overlay calendar modal (#12021) --- .../OverlayCalendarContainer.tsx | 34 ++++++++++--------- .../components/OverlayCalendar/store.ts | 12 +++++++ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx index f0a521cea0..a4e52447f5 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx @@ -1,6 +1,7 @@ import { useSession } from "next-auth/react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; -import { useState, useCallback, useEffect } from "react"; +import { useCallback, useEffect } from "react"; +import { shallow } from "zustand/shallow"; import dayjs from "@calcom/dayjs"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; @@ -18,17 +19,15 @@ import { useLocalSet } from "../hooks/useLocalSet"; import { useOverlayCalendarStore } from "./store"; interface OverlayCalendarSwitchProps { - setContinueWithProvider: (val: boolean) => void; - setCalendarSettingsOverlay: (val: boolean) => void; enabled?: boolean; } -function OverlayCalendarSwitch({ - setCalendarSettingsOverlay, - setContinueWithProvider, - enabled, -}: OverlayCalendarSwitchProps) { +function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) { const { t } = useLocale(); + const setContinueWithProvider = useOverlayCalendarStore((state) => state.setContinueWithProviderModal); + const setCalendarSettingsOverlay = useOverlayCalendarStore( + (state) => state.setCalendarSettingsOverlayModal + ); const layout = useBookerStore((state) => state.layout); const searchParams = useSearchParams(); const { data: session } = useSession(); @@ -110,9 +109,16 @@ function OverlayCalendarSwitch({ export function OverlayCalendarContainer() { const isEmbed = useIsEmbed(); const searchParams = useSearchParams(); - const [continueWithProvider, setContinueWithProvider] = useState(false); - const [calendarSettingsOverlay, setCalendarSettingsOverlay] = useState(false); - const { data: session } = useSession(); + const [continueWithProvider, setContinueWithProvider] = useOverlayCalendarStore( + (state) => [state.continueWithProviderModal, state.setContinueWithProviderModal], + shallow + ); + const [calendarSettingsOverlay, setCalendarSettingsOverlay] = useOverlayCalendarStore( + (state) => [state.calendarSettingsOverlayModal, state.setCalendarSettingsOverlayModal], + shallow + ); + + const { data: session, status: sessionStatus } = useSession(); const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates); const switchEnabled = searchParams.get("overlayCalendar") === "true" || @@ -170,11 +176,7 @@ export function OverlayCalendarContainer() { return ( <> - + { diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/store.ts b/packages/features/bookings/Booker/components/OverlayCalendar/store.ts index 1d9fd90b55..9187e90595 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/store.ts +++ b/packages/features/bookings/Booker/components/OverlayCalendar/store.ts @@ -5,6 +5,10 @@ import type { EventBusyDate } from "@calcom/types/Calendar"; interface IOverlayCalendarStore { overlayBusyDates: EventBusyDate[] | undefined; setOverlayBusyDates: (busyDates: EventBusyDate[]) => void; + continueWithProviderModal: boolean; + setContinueWithProviderModal: (value: boolean) => void; + calendarSettingsOverlayModal: boolean; + setCalendarSettingsOverlayModal: (value: boolean) => void; } export const useOverlayCalendarStore = create((set) => ({ @@ -12,4 +16,12 @@ export const useOverlayCalendarStore = create((set) => ({ setOverlayBusyDates: (busyDates: EventBusyDate[]) => { set({ overlayBusyDates: busyDates }); }, + calendarSettingsOverlayModal: false, + setCalendarSettingsOverlayModal: (value: boolean) => { + set({ calendarSettingsOverlayModal: value }); + }, + continueWithProviderModal: false, + setContinueWithProviderModal: (value: boolean) => { + set({ continueWithProviderModal: value }); + }, })); From ff3541910b2283410dcaaa81236c29fed273ea81 Mon Sep 17 00:00:00 2001 From: Siddharth Movaliya Date: Fri, 20 Oct 2023 19:57:03 +0530 Subject: [PATCH 004/118] fix: team availability slider overflow (#12020) --- packages/ui/components/data-table/DataTableToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/data-table/DataTableToolbar.tsx b/packages/ui/components/data-table/DataTableToolbar.tsx index df3370bed8..a7cbff8cb3 100644 --- a/packages/ui/components/data-table/DataTableToolbar.tsx +++ b/packages/ui/components/data-table/DataTableToolbar.tsx @@ -36,7 +36,7 @@ export function DataTableToolbar({ const isFiltered = table.getState().columnFilters.length > 0; return ( -
+
{searchKey && ( Date: Fri, 20 Oct 2023 15:31:03 +0100 Subject: [PATCH 005/118] fix: pwa nav improvements (#11972) --- apps/web/components/PageWrapper.tsx | 2 +- packages/features/shell/Shell.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/components/PageWrapper.tsx b/apps/web/components/PageWrapper.tsx index 5690b369bf..bdd311d2a9 100644 --- a/apps/web/components/PageWrapper.tsx +++ b/apps/web/components/PageWrapper.tsx @@ -58,7 +58,7 @@ function PageWrapper(props: AppProps) { { <>
- } - /> - ); -}; - const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => { const { t } = useLocale(); return ( @@ -984,7 +955,6 @@ const EventTypesPage = () => { heading={t("event_types_page_title")} hideHeadingOnMobile subtitle={t("event_types_page_subtitle")} - afterHeading={showProfileBanner && } beforeCTAactions={} CTA={}> Date: Fri, 20 Oct 2023 20:02:07 +0530 Subject: [PATCH 007/118] fix: fixed caldav app icon and text (#12016) Co-authored-by: Peer Richelsen Co-authored-by: Peer Richelsen --- .../app-store/caldavcalendar/DESCRIPTION.md | 2 +- .../app-store/caldavcalendar/static/icon.svg | 22 ++++++++++++++++--- packages/ui/components/apps/AppCard.tsx | 3 +-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/app-store/caldavcalendar/DESCRIPTION.md b/packages/app-store/caldavcalendar/DESCRIPTION.md index 85efc31f5d..b59c150be8 100644 --- a/packages/app-store/caldavcalendar/DESCRIPTION.md +++ b/packages/app-store/caldavcalendar/DESCRIPTION.md @@ -3,7 +3,7 @@ items: - 1.jpg --- -> 🚨 A slow Caldav server can cause a slow booking page for you +> 🚨 A slow Caldav server can cause a slow booking page for you Caldav is a protocol that allows different clients/servers to access scheduling information on remote servers as well as schedule meetings with other users on the same server or other servers. It extends WebDAV specification and uses iCalendar format for the data. diff --git a/packages/app-store/caldavcalendar/static/icon.svg b/packages/app-store/caldavcalendar/static/icon.svg index 6871360e3e..74a4defff5 100644 --- a/packages/app-store/caldavcalendar/static/icon.svg +++ b/packages/app-store/caldavcalendar/static/icon.svg @@ -1,3 +1,19 @@ - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/components/apps/AppCard.tsx b/packages/ui/components/apps/AppCard.tsx index 4d55813e59..04ff2ad44f 100644 --- a/packages/ui/components/apps/AppCard.tsx +++ b/packages/ui/components/apps/AppCard.tsx @@ -58,8 +58,7 @@ export function AppCard({ app, credentials, searchText, userAdminTeams }: AppCar alt={`${app.name} Logo`} className={classNames( app.logo.includes("-dark") && "dark:invert", - "mb-4 h-12 w-12 rounded-sm", - app.dirName == "caldavcalendar" && "dark:invert" // TODO: Maybe find a better way to handle this @Hariom? + "mb-4 h-12 w-12 rounded-sm" // TODO: Maybe find a better way to handle this @Hariom? )} />
From 20b7633ab5f7ff750b1a39da3172e308cb1bb0a8 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 14:35:26 +0000 Subject: [PATCH 008/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/ar/common.json | 2 -- apps/web/public/static/locales/cs/common.json | 2 -- apps/web/public/static/locales/de/common.json | 2 -- apps/web/public/static/locales/es/common.json | 2 -- apps/web/public/static/locales/fr/common.json | 2 -- apps/web/public/static/locales/he/common.json | 2 -- apps/web/public/static/locales/it/common.json | 2 -- apps/web/public/static/locales/ja/common.json | 2 -- apps/web/public/static/locales/ko/common.json | 2 -- apps/web/public/static/locales/nl/common.json | 2 -- apps/web/public/static/locales/pl/common.json | 2 -- apps/web/public/static/locales/pt-BR/common.json | 2 -- apps/web/public/static/locales/pt/common.json | 2 -- apps/web/public/static/locales/ro/common.json | 2 -- apps/web/public/static/locales/ru/common.json | 2 -- apps/web/public/static/locales/sr/common.json | 2 -- apps/web/public/static/locales/sv/common.json | 2 -- apps/web/public/static/locales/tr/common.json | 2 -- apps/web/public/static/locales/uk/common.json | 2 -- apps/web/public/static/locales/vi/common.json | 2 -- apps/web/public/static/locales/zh-CN/common.json | 2 -- apps/web/public/static/locales/zh-TW/common.json | 2 -- 22 files changed, 44 deletions(-) diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index 2c520bfe4a..dac06274dd 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "هذه المنظمة ليس لديها فرق بعد", "org_no_teams_yet_description": "إذا كنت مسؤول، تأكد من إنشاء فرق لعرضها هنا.", "set_up": "الإعداد", - "set_up_your_profile": "إعداد ملفك الشخصي", - "set_up_your_profile_description": "دع الناس يعرفون من أنت داخل {{orgName}}، ومتى يتعاملون مع الرابط العام الخاص بك.", "my_profile": "الملف الشخصي", "my_settings": "الإعدادات", "crm": "CRM", diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index a5284279cd..167b8c5ee8 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Tato organizace zatím neobsahuje žádné týmy", "org_no_teams_yet_description": "Pokud jste správce, nezapomeňte vytvořit týmy, které se zde budou zobrazovat.", "set_up": "Nastavení", - "set_up_your_profile": "Nastavení profilu", - "set_up_your_profile_description": "Dejte lidem vědět, co v rámci organizace {{orgName}} děláte, pokud si otevřou váš veřejný odkaz.", "my_profile": "Můj profil", "my_settings": "Moje nastavení", "crm": "CRM", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 110b1a124b..f8c03a41cb 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -1965,8 +1965,6 @@ "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 beim Klicken auf Ihren öffentlichen Link wissen, welche Funktion Sie innerhalb von {{orgName}} haben.", "my_profile": "Mein Profil", "my_settings": "Meine Einstellungen", "crm": "CRM", diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 11f55cbd01..76f03f4f5d 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Esta organización aún no tiene equipos", "org_no_teams_yet_description": "Si usted es un administrador, asegúrese de crear equipos para que se muestren aquí.", "set_up": "Configurar", - "set_up_your_profile": "Configure su perfil", - "set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} y cuándo interactúen con su enlace público.", "my_profile": "Mi perfil", "my_settings": "Mi configuración", "crm": "CRM", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 3771f99dbb..b11e9ef8b4 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -1959,8 +1959,6 @@ "org_no_teams_yet": "Cette organisation n'a pas encore d'équipe", "org_no_teams_yet_description": "Si vous êtes un administrateur, assurez-vous de créer des équipes à afficher ici.", "set_up": "Configurer", - "set_up_your_profile": "Configurer votre profil", - "set_up_your_profile_description": "Faites savoir aux gens qui vous êtes au sein de {{orgName}} et quand ils interagissent avec votre lien public.", "my_profile": "Mon profil", "my_settings": "Mes paramètres", "crm": "CRM", diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index d922c1b600..eafb188fa2 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "בארגון הזה עדיין אין צוותים", "org_no_teams_yet_description": "אם את/ה מנהל/ת מערכת, צור/צרי צוותים שיוצגו כאן.", "set_up": "הגדרה", - "set_up_your_profile": "הגדרת הפרופיל שלך", - "set_up_your_profile_description": "אפשר/י לאנשים לדעת מי את/ה ב-{{orgName}} וכשהם מקיימים איתך אינטראקציה באמצעות הקישור הציבורי שלך.", "my_profile": "הפרופיל שלי", "my_settings": "ההגדרות שלי", "crm": "CRM", diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index bcf626a2c7..2e7295e798 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Questa organizzazione non ha ancora alcun team", "org_no_teams_yet_description": "Se sei un amministratore, assicurati di creare dei team da mostrare qui.", "set_up": "Imposta", - "set_up_your_profile": "Imposta il tuo profilo", - "set_up_your_profile_description": "Fai sapere agli utenti la tua posizione all'interno di {{orgName}} quando interagiscono con il tuo link pubblico.", "my_profile": "Il mio profilo", "my_settings": "Le mie impostazioni", "crm": "CRM", diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index e61ce0d309..c8c2724d64 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "この組織にはまだチームがありません", "org_no_teams_yet_description": "管理者である場合には、ここに表示されるチームを必ず作成してください。", "set_up": "設定", - "set_up_your_profile": "プロフィールを設定する", - "set_up_your_profile_description": "{{orgName}} 内で、公開リンクを使用するユーザーにあなたのことを知ってもらいましょう。", "my_profile": "プロフィール", "my_settings": "設定", "crm": "CRM", diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index 71e7cacdc2..094dfba824 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "이 조직에 아직 팀이 없습니다", "org_no_teams_yet_description": "관리자인 경우 여기에 표시할 팀을 만들어야 합니다.", "set_up": "설정", - "set_up_your_profile": "사용자 프로필 설정", - "set_up_your_profile_description": "{{orgName}} 내에서 귀하가 누구인지, 그리고 귀하의 공개 링크에 언제 참여하는지 사람들에게 알려주세요.", "my_profile": "내 프로필", "my_settings": "내 설정", "crm": "CRM", diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index f3daa8ec0e..f41834c897 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Deze organisatie heeft nog geen teams", "org_no_teams_yet_description": "Als u een beheerder bent, zorg er dan voor dat u teams maakt die hier weergegeven worden.", "set_up": "Instellen", - "set_up_your_profile": "Stel uw profiel in", - "set_up_your_profile_description": "Laat mensen weten wie u bent binnen {{orgName}} en wanneer ze uw openbare link gebruiken.", "my_profile": "Mijn profiel", "my_settings": "Mijn instellingen", "crm": "CRM", diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index eb6fc024a1..79a89a697d 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "W tej organizacji nie ma jeszcze żadnych zespołów", "org_no_teams_yet_description": "Jeśli jesteś administratorem, pamiętaj o utworzeniu zespołów, które będą wyświetlane tutaj.", "set_up": "Skonfiguruj", - "set_up_your_profile": "Skonfiguruj profil", - "set_up_your_profile_description": "Określ informacje o Twojej roli w organizacji {{orgName}} wyświetlane osobom, które klikną Twój link publiczny.", "my_profile": "Mój profil", "my_settings": "Moje ustawienia", "crm": "CRM", diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index e4b4fabd8e..b1e7df6ef1 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Esta organização ainda não tem nenhuma equipe", "org_no_teams_yet_description": "Se você for administrador, não se esqueça de criar equipes para mostrar aqui.", "set_up": "Configurar", - "set_up_your_profile": "Configurar seu perfil", - "set_up_your_profile_description": "Deixe que as pessoas saibam quem você é em {{orgName}} e quando interagirem com seu link público.", "my_profile": "Meu perfil", "my_settings": "Minhas configurações", "crm": "CRM", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index df992ac7a9..1e74a7094e 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -1958,8 +1958,6 @@ "org_no_teams_yet": "Esta organização ainda não tem equipas", "org_no_teams_yet_description": "Se é um administrador, deve criar equipas para serem apresentadas aqui.", "set_up": "Configurar", - "set_up_your_profile": "Configurar o seu perfil", - "set_up_your_profile_description": "Permita que as pessoas saibam quem você é em {{orgName}} e quando se envolvem com o seu link público.", "my_profile": "O meu perfil", "my_settings": "As minhas definições", "crm": "CRM", diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index d04555c2af..d6b2688dbe 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Această organizație nu are încă echipe", "org_no_teams_yet_description": "Dacă sunteți administrator, creați echipe pentru a fi afișate aici.", "set_up": "Configurare", - "set_up_your_profile": "Configurați-vă profilul", - "set_up_your_profile_description": "Spuneți-le celorlalți care este rolul dvs. în cadrul {{orgName}} și atunci când interacționează cu linkul dvs. public.", "my_profile": "Profilul meu", "my_settings": "Setările mele", "crm": "CRM", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index f37ac89e2a..903a114a5a 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "У этой организации еще нет команд", "org_no_teams_yet_description": "Если вы администратор, не забудьте создать команды, которые будут отображаться здесь.", "set_up": "Настроить", - "set_up_your_profile": "Настроить профиль", - "set_up_your_profile_description": "Расскажите, чем вы занимаетесь в {{orgName}}. Эта информация будет отобраться в ваших публичных ссылках.", "my_profile": "Мой профиль", "my_settings": "Мои настройки", "crm": "CRM", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index f16c3aab01..f73510dc5a 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Ova organizacija još uvek nema timove", "org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.", "set_up": "Podesi", - "set_up_your_profile": "Podesite svoj profil", - "set_up_your_profile_description": "Unesite informacije o svojoj ulozi u organizaciji {{orgName}}. Te informacije će videti osobe koje kliknu na vaš javni link.", "my_profile": "Moj profil", "my_settings": "Moja podešavanja", "crm": "CRM", diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index f42f8d2e9f..910266d828 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Denna organisation har inga team än", "org_no_teams_yet_description": "Om du är administratör ska du skapa team som visas här.", "set_up": "Konfigurera", - "set_up_your_profile": "Konfigurera din profil", - "set_up_your_profile_description": "Meddela andra vem du är inom {{orgName}} och när de engagerar sig i din offentliga länk.", "my_profile": "Min profil", "my_settings": "Mina inställningar", "crm": "CRM", diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 0d48a9b8bf..70f03acd5b 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "Bu kuruluşun henüz ekibi yok", "org_no_teams_yet_description": "Yöneticiyseniz burada gösterilecek ekipler oluşturduğunuzdan emin olun.", "set_up": "Düzenle", - "set_up_your_profile": "Profilinizi düzenleyin", - "set_up_your_profile_description": "{{orgName}} adlı kuruluş dâhilinde ve herkese açık bağlantınızla etkileşim kurduklarında insanların kim olduğunuzu bilmelerini sağlayın.", "my_profile": "Profilim", "my_settings": "Ayarlarım", "crm": "CRM", diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index ecc7707536..618f153f51 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "В організації поки немає команд", "org_no_teams_yet_description": "Якщо ви адміністратор, не забудьте створити команди, які з’являться тут.", "set_up": "Налаштувати", - "set_up_your_profile": "Налаштуйте профіль", - "set_up_your_profile_description": "Повідомте, яку функцію ви виконуєте в організації {{orgName}}. Ця інформація буде також доступна за вашим публічним посиланням.", "my_profile": "Мій профіль", "my_settings": "Мої налаштування", "crm": "CRM", diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index dad747495c..e55cea189c 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -1971,8 +1971,6 @@ "org_no_teams_yet": "Tổ chức này chưa có nhóm nào", "org_no_teams_yet_description": "Nếu bạn là một quản trị viên, hãy bảo đảm tạo nhóm các nhóm cần được hiển thị tại đây.", "set_up": "Thiết lập", - "set_up_your_profile": "Thiết lập hồ sơ của bạn", - "set_up_your_profile_description": "Hãy để mọi người biết bạn là ai trong {{orgName}}, và cho họ biết khi nhấp vào liên kết công cộng của bạn.", "my_profile": "Hồ sơ của tôi", "my_settings": "Cài đặt của tôi", "crm": "CRM", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index 149d851ee2..b918bac358 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -1958,8 +1958,6 @@ "org_no_teams_yet": "此组织尚无团队", "org_no_teams_yet_description": "如果您是管理员,请务必创建要在此处显示的团队。", "set_up": "设置", - "set_up_your_profile": "设置您的个人资料", - "set_up_your_profile_description": "让人们知道您在 {{orgName}} 中的身份,以及他们何时可与您的公共链接互动。", "my_profile": "我的个人资料", "my_settings": "我的设置", "crm": "CRM", diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 1b78c55c63..946db6ea7e 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -1957,8 +1957,6 @@ "org_no_teams_yet": "此組織尚無團隊", "org_no_teams_yet_description": "若您是管理員,請務必建立團隊,才會顯示在這裡。", "set_up": "設定", - "set_up_your_profile": "設定個人資料", - "set_up_your_profile_description": "向大家介紹您在 {{orgName}} 擔任的職務,以及他們何時可以使用您的公開連結。", "my_profile": "我的個人資料", "my_settings": "我的設定", "crm": "CRM", From e32d4648af57bdc03f11bd98562aff41d29f8847 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Fri, 20 Oct 2023 20:08:52 +0530 Subject: [PATCH 009/118] fix: webhook overflow (#11968) Co-authored-by: Peer Richelsen Co-authored-by: Peer Richelsen --- packages/features/webhooks/components/WebhookListItem.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/features/webhooks/components/WebhookListItem.tsx b/packages/features/webhooks/components/WebhookListItem.tsx index 00bf7d0023..8526a0ec45 100644 --- a/packages/features/webhooks/components/WebhookListItem.tsx +++ b/packages/features/webhooks/components/WebhookListItem.tsx @@ -73,7 +73,11 @@ export default function WebhookListItem(props: { )}>
-

{webhook.subscriberUrl}

+ +

+ {webhook.subscriberUrl} +

+
{!!props.readOnly && ( {t("readonly")} From 39ea9c112d66e97f137da0493dcf96510a9c8d0e Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Fri, 20 Oct 2023 15:39:28 +0100 Subject: [PATCH 010/118] chore: delete "unapproved issue" workflow (#12008) --- .github/workflows/comment-unapproved-issues | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/comment-unapproved-issues diff --git a/.github/workflows/comment-unapproved-issues b/.github/workflows/comment-unapproved-issues deleted file mode 100644 index d2c98fca46..0000000000 --- a/.github/workflows/comment-unapproved-issues +++ /dev/null @@ -1,18 +0,0 @@ -name: Add comment -on: - issues: - types: - - labeled -jobs: - add-comment: - if: github.event.label.name == '🚨 needs approval' - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Add comment - uses: peter-evans/create-or-update-comment@5f728c3dae25f329afbe34ee4d08eef25569d79f - with: - issue-number: ${{ github.event.issue.number }} - body: | - This feature request has not been reviewed yet by the Product Team and needs approval beforehand. Once approved, this issue is available for anyone to work on. **Make sure to reference this issue in your pull request.** :sparkles: Thank you for your contribution! :sparkles: From 34bb069b4a85c5837eb1d611a71444691941bc4f Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Fri, 20 Oct 2023 21:13:45 +0530 Subject: [PATCH 011/118] revert: feat: Shows link location (#12024) --- .../components/booking/BookingListItem.tsx | 25 ------------------- apps/web/public/static/locales/en/common.json | 2 -- 2 files changed, 27 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 3be8b6f900..e3f4fa7b22 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -88,10 +88,6 @@ function BookingListItem(booking: BookingItemProps) { const isRecurring = booking.recurringEventId !== null; const isTabRecurring = booking.listingStatus === "recurring"; const isTabUnconfirmed = booking.listingStatus === "unconfirmed"; - const eventLocationType = getEventLocationType(booking.location); - const meetingLink = booking.references[0]?.meetingUrl - ? booking.references[0]?.meetingUrl - : booking.location; const paymentAppData = getPaymentAppData(booking.eventType); @@ -357,27 +353,6 @@ function BookingListItem(booking: BookingItemProps) { attendees={booking.attendees} />
- {!isPending && (eventLocationType || booking.location?.startsWith("https://")) && ( - -
- {eventLocationType ? ( - <> - {`${eventLocationType.label} - {t("join_event_location", { eventLocationType: eventLocationType.label })} - - ) : ( - t("join_meeting") - )} -
- - )} - {isPending && ( {t("unconfirmed")} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 2018b00cf4..7a1b81c44c 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2093,7 +2093,5 @@ "overlay_my_calendar":"Overlay my calendar", "overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.", "view_overlay_calendar_events":"View your calendar events to prevent clashed booking.", - "join_event_location": "Join {{eventLocationType}}", - "join_meeting": "Join Meeting", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From c352dc647e123dde476f363b06de45f28eaf2bfb Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:44:26 +0000 Subject: [PATCH 012/118] fix: broken layout email embed generator (CALCOM-11779) (#11951) Co-authored-by: gitstart-calcom Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: Peer Richelsen --- packages/features/embed/Embed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index afebad31f0..8ed8abb4dd 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -242,7 +242,7 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username: {selectedDate ? (
{selectedDate ? ( -
+
Date: Fri, 20 Oct 2023 16:47:04 +0000 Subject: [PATCH 013/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 01e610bfd4..3f20a11555 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -17,6 +17,7 @@ "verify_email_banner_body": "Egiaztatu zure email helbidea mezuak eta egutegiko eguneratzeak ahalik eta hobekien jasoko dituzula bermatzeko", "verify_email_email_header": "Egiaztatu zure email helbidea", "verify_email_email_button": "Egiaztatu emaila", + "copy_somewhere_safe": "Gorde API gako hau toki seguru batean. Ezingo duzu berriro ikusi.", "verify_email_email_body": "Mesedez, egiaztatu zure email helbidea beheko botoia sakatuz.", "verify_email_by_code_email_body": "Mesedez, egiaztatu zure email helbidea beheko kodea erabiliz.", "verify_email_email_link_text": "Hemen duzu esteka, botoiak sakatzea gustuko ez baduzu:", @@ -45,17 +46,25 @@ "invite_team_notifcation_badge": "Gon.", "your_event_has_been_scheduled": "Zure gertaera programatu da", "your_event_has_been_scheduled_recurring": "Zure gertaera errepikaria programatu da", + "accept_our_license": "Onartu gure lizentzia <1>NEXT_PUBLIC_LICENSE_CONSENT .env aldagaia aldatuz honakora: '{{agree}}'.", + "remove_banner_instructions": "Baner hau ezabatzeko, mesedez zabaldu zure .env fitxategia eta aldatu <1>NEXT_PUBLIC_LICENSE_CONSENT aldagaia honakora: '{{agree}}'.", "error_message": "Errore-mezua honakoa ian da: '{{errorMessage}}'", "refund_failed_subject": "Itzulketak huts egin du: {{name}} - {{date}} - {{eventType}}", "refund_failed": "Huts egin du itzulketak {{eventType}} gertaerarako, {{userName}}(r)ekin {{date}}(e)an.", "a_refund_failed": "Itzulketa batek huts egin du", "awaiting_payment_subject": "Ordainketaren zain: {{title}} {{date}}(e)an", + "meeting_awaiting_payment": "Zure bilera ordainketa zain dago", "help": "Laguntza", "price": "Prezioa", "paid": "Ordainduta", "refunded": "Itzulita", "payment": "Ordainketa", + "missing_card_fields": "Txartelaren eremuak falta dira", "pay_now": "Ordaindu orain", + "terms_summary": "Terminoen laburpena", + "open_env": "Ireki .env eta onartu gure Lizentzia", + "env_changed": "Nire .env aldatu dut", + "accept_license": "Onart Lizentzia", "still_waiting_for_approval": "Gertaera bat onarpenaren zain dago", "event_is_still_waiting": "Gertaera-eskaera oraindik zain dago: {{attendeeName}} - {{date}} - {{eventType}}", "no_more_results": "Emaitza gehiagorik ez", @@ -82,12 +91,36 @@ "event_has_been_rescheduled": "Eguneratuta - Zure gertaeraren programazioa aldatu egin da", "request_reschedule_subtitle": "{{organizer}}(e)k erreserba bertan behera utzi du eta beste denbora-tarte bat hautatzeko eskatu dizu.", "request_reschedule_title_organizer": "Beste denbora-tarte bat hautatzeko eskatu diozu {{attendee}}(r)i", + "request_reschedule_subtitle_organizer": "Erreserba bertan behera utzi duzu eta {{attendee}}(e)k erreserba-ordu berri bat hautatu beharko du.", + "rescheduled_event_type_subject": "Berrantolatzeko eskaera bidalita: {{eventType}} {{name}}(r)ekin {{date}}(e)an", "hi_user_name": "Kaixo {{name}}", "ics_event_title": "{{eventType}} {{name}}(r)ekin", + "new_event_subject": "Gertaera berria: {{attendeeName}} - {{date}} - {{eventType}}", "notes": "Oharrak", "manage_my_bookings": "Kudeatu nire erreserbak", + "need_to_make_a_change": "Aldaketaren bat egin behar duzu?", + "new_event_scheduled": "Gertaera berri bat programatu da.", + "new_event_scheduled_recurring": "Gertaera errepikari berri bat programatu da.", + "invitee_email": "Gonbidatuaren emaila", + "invitee_timezone": "Gonbidatuaren ordu-eremua", + "event_type": "Gertaera mota", + "enter_meeting": "Sartu bilerara", + "video_call_provider": "Bideodeiaren hornitzailea", + "meeting_id": "Bileraren IDa", + "meeting_password": "Bileraren pasahitza", + "meeting_url": "Bilerarako URLa", + "meeting_request_rejected": "Zure bilera eskaera ez da onartu", "rejected_event_type_with_organizer": "Errefusatua: {{eventType}} {{organizer}}(r)ekin {{date}}(e)an", "hi": "Kaixo", + "join_team": "Batu taldera", + "manage_this_team": "Kudeatu talde hau", + "team_info": "Taldearen informazioa", + "you_have_been_invited": "{{teamName}} taldera batzeko gonbidatua izan zara", + "hidden_team_member_title": "Ezkutuan zaude talde honetan", + "hidden_team_owner_message": "Pro kontu bat behar duzu taldeak erabiltzeko, ezkutuan geratuko zara bitartean.", + "team_upgrade_banner_description": "Ez duzu taldea konfiguratzen bukatu. Zure \"{{teamName}}\" taldea bertsio-berritu behar duzu.", + "upgrade_banner_action": "Bertsio-berritu hemen", + "team_upgraded_successfully": "Zure taldea zuzen bertsio-berritu da!", "use_link_to_reset_password": "Erabili beheko esteka pasahitza berrezartzeko", "hey_there": "Kaixo,", "forgot_your_password_calcom": "Pasahitza ahaztu duzu? - {{appName}}", @@ -122,8 +155,32 @@ "rejected": "Baztertua", "unconfirmed": "Baieztatu gabea", "guests": "Gonbidatuak", + "404_claim_entity_user": "Erreklamatu zure erabiltzaile-izena eta programatu gertaerak", + "popular_pages": "Orrialde ospetsuak", + "register_now": "Izena eman orain", + "register": "Izena eman", + "page_doesnt_exist": "Orrialde hau ez dago.", + "check_spelling_mistakes_or_go_back": "Egiaztatu akats ortografikorik ez dagoela edo joan aurreko orrira.", + "404_page_not_found": "404: Orrialde hau ezin izan da aurkitu.", + "booker_event_not_found": "Ezin izan dugu aurkitu erreserbatu nahi izan duzun gertaera.", + "getting_started": "Nola hasi", + "15min_meeting": "15 minutuko bilera", + "30min_meeting": "30 minutuko bilera", + "secret": "Sekretua", + "leave_blank_to_remove_secret": "Zuri utzi sekretua ezabatzeko", + "secret_meeting": "Bilera sekretua", + "already_have_an_account": "Baduzu kontua dagoeneko?", "create_account": "Sortu kontua", "confirm_password": "Baieztatu pasahitza", + "reset_your_password": "Ezarri zure pasahitz berria zure email helbidera bidalitako argibideak jarraituz.", + "create_your_account": "Sortu zure kontua", + "sign_up": "Izena eman", + "youve_been_logged_out": "Saioa amaitu duzu", + "hope_to_see_you_soon": "Laster ikusiko zaitugula espero dugu!", + "logged_out": "Saioa amaituta", + "no_account_exists": "Ez dago konturik email helbide horrekin bat datorrenik.", + "create_an_account": "Sortu kontu bat", + "dont_have_an_account": "Ez duzu konturik?", "create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin", "user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.", "booking_submitted": "Zure erreserba bidali da", From 99a1c36ffc24b3bcd041fccd88d657ffb9774882 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 16:49:37 +0000 Subject: [PATCH 014/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 3f20a11555..3e098c0220 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -181,9 +181,23 @@ "no_account_exists": "Ez dago konturik email helbide horrekin bat datorrenik.", "create_an_account": "Sortu kontu bat", "dont_have_an_account": "Ez duzu konturik?", + "sign_in_account": "Hasi saioa zure kontuan", + "sign_in": "Hasi saioa", + "go_back_login": "Itzuli saio-hasiera orrialdera", + "request_password_reset": "Bidali berrezartzeko emaila", + "send_invite": "Bidali gonbidapena", + "forgot_password": "Pasahitza ahaztu duzu?", + "forgot": "Ahaztuta?", + "done": "Eginda", + "all_done": "Dena eginda!", + "all": "Guztia", + "yours": "Zure kontua", "create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin", "user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.", + "meeting_is_scheduled": "Bilera hau programatuta dago", + "meeting_is_scheduled_recurring": "Gertaera errepikariak programatuta daude", "booking_submitted": "Zure erreserba bidali da", + "booking_submitted_recurring": "Zure bilera errepikaria bidali da", "booking_confirmed": "Zure erreserba baieztatu da", "bookerlayout_column_view": "Zutabea", "back_to_bookings": "Itzuli erreserbatara", From 309321653433415500172bab39f8f8cfb157279e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 16:52:30 +0000 Subject: [PATCH 015/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 3e098c0220..f92413ed55 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -199,6 +199,22 @@ "booking_submitted": "Zure erreserba bidali da", "booking_submitted_recurring": "Zure bilera errepikaria bidali da", "booking_confirmed": "Zure erreserba baieztatu da", + "booking_confirmed_recurring": "Zure bilera errepikaria baieztatu da", + "reset_password": "Berrezarri pasahitza", + "change_your_password": "Aldatu zure pasahitza", + "show_password": "Erakutsi pasahitza", + "hide_password": "Ezkutatu pasahitza", + "try_again": "Saiatu berriro", + "whoops": "Hara", + "login": "Saioa hasi", + "success": "Arrakasta", + "failed": "Huts egin du", + "password_has_been_reset_login": "Zure pasahitza berrezarri da. Orain saioa has dezakezu sortu berri duzun pasahitzarekin.", + "layout": "Diseinua", + "bookerlayout_default_title": "Lehenetsitako ikuspegia", + "bookerlayout_user_settings_title": "Erreserbatarako diseinua", + "bookerlayout_month_view": "Hilabetea", + "bookerlayout_week_view": "Astero", "bookerlayout_column_view": "Zutabea", "back_to_bookings": "Itzuli erreserbatara", "really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?", From 55a8a0d2d36c75faa9d9d3b8c43a55719f2fbce2 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Fri, 20 Oct 2023 13:53:48 -0300 Subject: [PATCH 016/118] v3.4.3 --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 245026e66e..829f47e542 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.4.2", + "version": "3.4.3", "private": true, "scripts": { "analyze": "ANALYZE=true next build", From d333a31221811cf2e4a8b110f3579235c1f7c769 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 16:56:42 +0000 Subject: [PATCH 017/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index f92413ed55..a8cdc309be 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -216,7 +216,20 @@ "bookerlayout_month_view": "Hilabetea", "bookerlayout_week_view": "Astero", "bookerlayout_column_view": "Zutabea", + "bookerlayout_error_min_one_enabled": "Gutxienez ikuspegi bat gaituta egotea behar da.", + "bookerlayout_error_default_not_enabled": "Lehenetsitako ikuspegi bezala hautatu duzun diseinua ez dago gaitutako diseinuen artean.", + "bookerlayout_error_unknown_layout": "Hautatu duzun diseinua ez da baliozko diseinu bat.", + "sunday_time_error": "Ordu baliogabea igandean", + "monday_time_error": "Ordu baliogabea astelehenean", + "tuesday_time_error": "Ordu baliogabea asteartean", + "wednesday_time_error": "Ordu baliogabea asteazkenean", + "thursday_time_error": "Ordu baliogabea ostegunean", + "friday_time_error": "Ordu baliogabea ostiralean", + "saturday_time_error": "Ordu baliogabea larunbatean", + "error_end_time_before_start_time": "Amaiera-orduak ezin du hasiera-ordua baino lehenago izan", + "error_end_time_next_day": "Amaiera-denborak ezin du 24 ordu baino gehiago izan", "back_to_bookings": "Itzuli erreserbatara", + "cancelled": "Bertan behera", "really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?", "cannot_cancel_booking": "Ezin duzu erreserba hau bertan behera utzi", "booking_already_accepted_rejected": "Erreserba hau onartu edo errefusatu da dagoeneko", @@ -224,6 +237,7 @@ "past_bookings": "Zure iraganeko erreserbak agertuko dira hemen.", "unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.", "unconfirmed_bookings_tooltip": "Baieztatu gabeko erreserbak", + "end_time": "Amaiera-orduan", "booking_rescheduled": "Erreserbaren programazioa aldatuta", "booking_created": "Erreserba sortuta", "edit_booking": "Aldatu erreserba", From 0763a64b30976e59bb79fef16e92a9bbde284654 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 16:59:15 +0000 Subject: [PATCH 018/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index a8cdc309be..48121b8e84 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -238,6 +238,16 @@ "unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.", "unconfirmed_bookings_tooltip": "Baieztatu gabeko erreserbak", "end_time": "Amaiera-orduan", + "buffer_time": "Tarteko denbora", + "before_event": "Gertaeraren aurretik", + "after_event": "Gertaeraren ondoren", + "event_buffer_default": "Tarteko denborarik ez", + "buffer": "Tarteko denbora", + "your_day_starts_at": "Zure eguna hasten den ordua:", + "your_day_ends_at": "Zure eguna amaitzen den ordua:", + "change_available_times": "Aldatu libre zauden orduan", + "change_your_available_times": "Aldatu libre zauden orduak", + "change_start_end": "Aldatu zure egunaren hasiera- eta amaiera-orduak", "booking_rescheduled": "Erreserbaren programazioa aldatuta", "booking_created": "Erreserba sortuta", "edit_booking": "Aldatu erreserba", From 6627a211d704a3bd41b58ca4a46104adfb75686c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:02:38 +0000 Subject: [PATCH 019/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 48121b8e84..15e69e527d 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -248,6 +248,14 @@ "change_available_times": "Aldatu libre zauden orduan", "change_your_available_times": "Aldatu libre zauden orduak", "change_start_end": "Aldatu zure egunaren hasiera- eta amaiera-orduak", + "change_start_end_buffer": "Ezarri zure egunaren hasiera- eta amaiera-ordua eta gutxieneko denbora tarte bat zure bileren artean.", + "current_start_date": "Unean, zure eguna honako orduan hasteko ezarrita dago:", + "start_end_changed_successfully": "Zure egunaren hasiera- eta amaiera-orduak egoki aldatu dira.", + "light": "Argia", + "dark": "Iluna", + "email": "Emaila", + "email_placeholder": "izena@adibidea.eus", + "full_name": "Izen osoa", "booking_rescheduled": "Erreserbaren programazioa aldatuta", "booking_created": "Erreserba sortuta", "edit_booking": "Aldatu erreserba", From 8d4561c866d342ba07e9e75c3abb38c6e6e2af09 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:05:38 +0000 Subject: [PATCH 020/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 15e69e527d..59650279c2 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -155,6 +155,12 @@ "rejected": "Baztertua", "unconfirmed": "Baieztatu gabea", "guests": "Gonbidatuak", + "guest": "Gonbidatua", + "404_the_user": "Erabiltzaile izena", + "username": "Erabiltzaile izena", + "is_still_available": "orandik eskuragarri dago.", + "documentation": "Dokumentazioa", + "blog": "Bloga", "404_claim_entity_user": "Erreklamatu zure erabiltzaile-izena eta programatu gertaerak", "popular_pages": "Orrialde ospetsuak", "register_now": "Izena eman orain", @@ -192,6 +198,12 @@ "all_done": "Dena eginda!", "all": "Guztia", "yours": "Zure kontua", + "finish": "Amaitu", + "organization_general_description": "Kudeatu zure taldearen hizkuntza eta ordu-eremuko ezarpenak", + "few_sentences_about_yourself": "Esaldi gutxi batzuk zeuri buruz. Zure orrialde pertsonalean agertuko dira.", + "nearly_there": "Ia bukatuta!", + "set_availability": "Ezarri zein ordutan zauden libre", + "continue_without_calendar": "Jarraitu egutegirik gabe", "create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin", "user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.", "meeting_is_scheduled": "Bilera hau programatuta dago", @@ -256,6 +268,7 @@ "email": "Emaila", "email_placeholder": "izena@adibidea.eus", "full_name": "Izen osoa", + "booking_cancelled": "Erreserba bertan behera", "booking_rescheduled": "Erreserbaren programazioa aldatuta", "booking_created": "Erreserba sortuta", "edit_booking": "Aldatu erreserba", From bf3db721e29b2994f0f18120dce2f1409bed61da Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:08:38 +0000 Subject: [PATCH 021/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 59650279c2..54c9250a18 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -204,7 +204,25 @@ "nearly_there": "Ia bukatuta!", "set_availability": "Ezarri zein ordutan zauden libre", "continue_without_calendar": "Jarraitu egutegirik gabe", + "connect_your_calendar": "Konektatu zure egutegia", + "connect_your_video_app": "Konektatu zure bideo-aplikazioak", + "set_up_later": "Konfiguratu geroago", + "current_time": "Uneko ordua", + "details": "Xehetasunak", + "welcome": "Ongi etorri", + "welcome_back": "Ongi etorri", + "welcome_to_calcom": "Ongi etorri {{appName}}(e)ra", + "connect": "Konektatu", + "try_for_free": "Proba ezazu doan", "create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin", + "who": "Nor(k)", + "what": "Zer", + "when": "Noiz", + "where": "Non", + "add_to_calendar": "Gehitu egutegira", + "add_events_to": "Gehitu gertaerak hona:", + "add_another_calendar": "Gehitu beste egutegi bat", + "other": "Besterik", "user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.", "meeting_is_scheduled": "Bilera hau programatuta dago", "meeting_is_scheduled_recurring": "Gertaera errepikariak programatuta daude", @@ -245,6 +263,10 @@ "really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?", "cannot_cancel_booking": "Ezin duzu erreserba hau bertan behera utzi", "booking_already_accepted_rejected": "Erreserba hau onartu edo errefusatu da dagoeneko", + "go_back_home": "Itzuli hasierara", + "or_go_back_home": "Edo itzuli hasierara", + "no_meeting_found": "Ez da bilerarik aurkitu", + "no_meeting_found_description": "Bilera hau ez dago. Jarri harremanetan bileraren jabearekin eguneratutako esteka lortzeko.", "bookings": "Erreserbak", "past_bookings": "Zure iraganeko erreserbak agertuko dira hemen.", "unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.", From 92e5aae901897e282c0f9f48931b5896c14379a4 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:11:15 +0000 Subject: [PATCH 022/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 54c9250a18..5eeb25df3e 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -268,9 +268,11 @@ "no_meeting_found": "Ez da bilerarik aurkitu", "no_meeting_found_description": "Bilera hau ez dago. Jarri harremanetan bileraren jabearekin eguneratutako esteka lortzeko.", "bookings": "Erreserbak", + "booking_not_found": "Erreserba ez da aurkitu", "past_bookings": "Zure iraganeko erreserbak agertuko dira hemen.", "unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.", "unconfirmed_bookings_tooltip": "Baieztatu gabeko erreserbak", + "start_time": "Hasiera-ordua", "end_time": "Amaiera-orduan", "buffer_time": "Tarteko denbora", "before_event": "Gertaeraren aurretik", @@ -293,6 +295,21 @@ "booking_cancelled": "Erreserba bertan behera", "booking_rescheduled": "Erreserbaren programazioa aldatuta", "booking_created": "Erreserba sortuta", + "booking_rejected": "Erreserba ez onartua", + "booking_requested": "Erreserba eskatua", + "meeting_ended": "Bilera amaitu da", + "form_submitted": "Galdetegia bidali da", + "uh_oh": "Ai ama!", + "no_event_types_have_been_setup": "Erabiltzaile honek ez du gertaera-motarik konfiguratu oraindik.", + "edit_logo": "Editatu logoa", + "upload_a_logo": "Kargatu logo bat", + "upload_logo": "Kargatu logoa", + "remove_logo": "Ezabatu logoa", + "enable": "Gaitu", + "code": "Kodea", + "code_is_incorrect": "Kodea ez da zuzena.", + "add_time_availability": "Gehitu denbora-tarte berri bat", + "security": "Segurtasuna", "edit_booking": "Aldatu erreserba", "reschedule_booking": "Aldatu erreserbaren programazioa", "location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an", From 446c2b0f0ec8514504b69b880a9b015220fb19fe Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:14:23 +0000 Subject: [PATCH 023/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 5eeb25df3e..9cbedf956c 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -310,6 +310,28 @@ "code_is_incorrect": "Kodea ez da zuzena.", "add_time_availability": "Gehitu denbora-tarte berri bat", "security": "Segurtasuna", + "manage_account_security": "Kudeatu zure kontuaren segurtasuna.", + "password": "Pasahitza", + "password_updated_successfully": "Pasahitza egoki eguneratu da", + "password_has_been_changed": "Zure pasahitza egoki aldatu da.", + "error_changing_password": "Errorea pasahitza aldatzean", + "something_went_wrong": "Zerbait gaizki joan da.", + "something_doesnt_look_right": "Zerbaitek ez du itxura onik?", + "please_try_again": "Saia zaitez berriro, mesedez.", + "super_secure_new_password": "Zure pasahitz berri super segurua", + "new_password": "Pasahitz berria", + "your_old_password": "Zure pasahitz zaharra", + "current_password": "Uneko pasahitza", + "change_password": "Aldatu pasahitza", + "change_secret": "Aldatu sekretua", + "new_password_matches_old_password": "Pasahitz berria zure pasahitz zaharrarekin bat dator. Aukeratu ezazu pasahitz ezberdin bat, mesedez.", + "current_incorrect_password": "Uneko pasahitza ez da zuzena", + "password_hint_caplow": "Maiuskulak eta minuskulak nahasian", + "password_hint_min": "Gutxienez 7 karaktereko luzera", + "password_hint_admin_min": "Gutxienez 15 karaktereko luzera", + "password_hint_num": "Gutxienez zenbaki bat", + "max_limit_allowed_hint": "{{limit}} karaktere edo gutxiagoko luzera izan behar du", + "email_address": "Email helbidea", "edit_booking": "Aldatu erreserba", "reschedule_booking": "Aldatu erreserbaren programazioa", "location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an", From eac45c5e232b6f55572b99130086dcfa9adecc19 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 17:17:26 +0000 Subject: [PATCH 024/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 9cbedf956c..1c30877c19 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -332,6 +332,26 @@ "password_hint_num": "Gutxienez zenbaki bat", "max_limit_allowed_hint": "{{limit}} karaktere edo gutxiagoko luzera izan behar du", "email_address": "Email helbidea", + "enter_valid_email": "Mesedez, adierazi baliozko email helbide bat", + "location": "Kokapena", + "address": "Helbidea", + "enter_address": "Sartu helbidea", + "in_person_attendee_address": "Aurrez aurre (partaidearen helbidean)", + "yes": "bai", + "no": "ez", + "additional_notes": "Ohar gehigarriak", + "booking_fail": "Ezin izan da bilera erreserbatu.", + "reschedule_fail": "Ezin izan da bilera berrantolatu.", + "in_person_meeting": "Aurrez aurreko bilera", + "in_person": "Aurrez aurre (antolatzailearen helbidean)", + "phone_number": "Telefono zenbakia", + "attendee_phone_number": "Partaidearen telefono zenbakia", + "organizer_phone_number": "Antolatzailearen telefono zenbakia", + "enter_phone_number": "Sartu telefono zenbakia", + "reschedule": "Berrantolatu", + "or": "EDO", + "go_back": "Atzera", + "email_or_username": "Emaila edo erabiltzaile izena", "edit_booking": "Aldatu erreserba", "reschedule_booking": "Aldatu erreserbaren programazioa", "location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an", From 39cfe18ffe4596441f44ce6e562f00ba4076a810 Mon Sep 17 00:00:00 2001 From: Greg Pabian <35925521+grzpab@users.noreply.github.com> Date: Sat, 21 Oct 2023 01:47:05 +0200 Subject: [PATCH 025/118] chore: [app dir bootstrapping 4] check nullability of navigation hook return values (#12005) --- apps/web/components/booking/CancelBooking.tsx | 9 +++-- .../UsernameAvailability/PremiumTextfield.tsx | 4 +-- apps/web/lib/hooks/useIsBookingPage.ts | 8 ++--- apps/web/lib/hooks/useRouterQuery.ts | 16 +++++---- apps/web/pages/404.tsx | 13 ++++--- apps/web/pages/auth/login.tsx | 2 +- apps/web/pages/auth/oauth2/authorize.tsx | 2 +- apps/web/pages/auth/setup/index.tsx | 2 +- apps/web/pages/auth/verify.tsx | 2 +- apps/web/pages/booking/[uid].tsx | 2 +- apps/web/pages/event-types/index.tsx | 2 +- .../alby/components/AlbyPaymentComponent.tsx | 12 ++++++- packages/app-store/alby/pages/setup/index.tsx | 13 ++++--- .../routing-forms/components/FormActions.tsx | 2 +- .../pages/routing-link/[...appPages].tsx | 2 +- packages/features/apps/AdminAppsList.tsx | 2 +- .../OverlayCalendarContainer.tsx | 4 +-- .../features/bookings/Booker/utils/event.ts | 4 +-- .../components/event-meta/Members.tsx | 2 +- .../settings/other-team-members-view.tsx | 14 ++++---- .../settings/other-team-profile-view.tsx | 4 +-- .../ee/payments/components/Payment.tsx | 9 +++-- .../ee/users/pages/users-add-view.tsx | 5 ++- packages/features/embed/Embed.tsx | 9 ++--- .../insights/context/FiltersProvider.tsx | 35 ++++++++++--------- packages/features/shell/Shell.tsx | 17 +++++---- .../webhooks/pages/webhook-edit-view.tsx | 2 +- packages/lib/bookingSuccessRedirect.ts | 2 +- 28 files changed, 114 insertions(+), 86 deletions(-) diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index 76297831fa..693cd7ba34 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -1,4 +1,4 @@ -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useCallback, useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -26,9 +26,6 @@ type Props = { }; export default function CancelBooking(props: Props) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const asPath = `${pathname}?${searchParams.toString()}`; const [cancellationReason, setCancellationReason] = useState(""); const { t } = useLocale(); const router = useRouter(); @@ -44,6 +41,7 @@ export default function CancelBooking(props: Props) { } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return ( <> {error && ( @@ -100,7 +98,8 @@ export default function CancelBooking(props: Props) { }); if (res.status >= 200 && res.status < 300) { - router.replace(asPath); + // tested by apps/web/playwright/booking-pages.e2e.ts + router.refresh(); } else { setLoading(false); setError( diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index dbef95668d..fb33a0f899 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -222,9 +222,9 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { onChange={(event) => { event.preventDefault(); // Reset payment status - const _searchParams = new URLSearchParams(searchParams); + const _searchParams = new URLSearchParams(searchParams ?? undefined); _searchParams.delete("paymentStatus"); - if (searchParams.toString() !== _searchParams.toString()) { + if (searchParams?.toString() !== _searchParams.toString()) { router.replace(`${pathname}?${_searchParams.toString()}`); } setInputUsernameValue(event.target.value); diff --git a/apps/web/lib/hooks/useIsBookingPage.ts b/apps/web/lib/hooks/useIsBookingPage.ts index 1e231e3f40..3f890bcedc 100644 --- a/apps/web/lib/hooks/useIsBookingPage.ts +++ b/apps/web/lib/hooks/useIsBookingPage.ts @@ -1,12 +1,12 @@ import { usePathname, useSearchParams } from "next/navigation"; -export default function useIsBookingPage() { +export default function useIsBookingPage(): boolean { const pathname = usePathname(); const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route)); const searchParams = useSearchParams(); - const userParam = searchParams.get("user"); - const teamParam = searchParams.get("team"); + const userParam = Boolean(searchParams?.get("user")); + const teamParam = Boolean(searchParams?.get("team")); - return !!(isBookingPage || userParam || teamParam); + return isBookingPage || userParam || teamParam; } diff --git a/apps/web/lib/hooks/useRouterQuery.ts b/apps/web/lib/hooks/useRouterQuery.ts index 56b321c1fa..3bd40e57b3 100644 --- a/apps/web/lib/hooks/useRouterQuery.ts +++ b/apps/web/lib/hooks/useRouterQuery.ts @@ -1,17 +1,21 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback } from "react"; export default function useRouterQuery(name: T) { const searchParams = useSearchParams(); const pathname = usePathname(); const router = useRouter(); - const setQuery = (newValue: string | number | null | undefined) => { - const _searchParams = new URLSearchParams(searchParams); - _searchParams.set(name, newValue as string); - router.replace(`${pathname}?${_searchParams.toString()}`); - }; + const setQuery = useCallback( + (newValue: string | number | null | undefined) => { + const _searchParams = new URLSearchParams(searchParams ?? undefined); + _searchParams.set(name, newValue as string); + router.replace(`${pathname}?${_searchParams.toString()}`); + }, + [name, pathname, router, searchParams] + ); - return { [name]: searchParams.get(name), setQuery } as { + return { [name]: searchParams?.get(name), setQuery } as { [K in T]: string | undefined; } & { setQuery: typeof setQuery }; } diff --git a/apps/web/pages/404.tsx b/apps/web/pages/404.tsx index 6871c19630..fc23e64fd1 100644 --- a/apps/web/pages/404.tsx +++ b/apps/web/pages/404.tsx @@ -51,8 +51,8 @@ export default function Custom404() { const [url, setUrl] = useState(`${WEBSITE_URL}/signup`); useEffect(() => { const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host); - const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/); - if (!isValidOrgDomain || !currentOrgDomain) { + const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? []; + if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) { const splitPath = routerUsername.split("/"); if (splitPath[1] === "team" && splitPath.length === 3) { // Accessing a non-existent team @@ -66,13 +66,12 @@ export default function Custom404() { setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`); } } else { - setUsername(currentOrgDomain); + setUsername(currentOrgDomain ?? ""); setCurrentPageType(pageType.ORG); setUrl( - `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${currentOrgDomain.replace( - "/", - "" - )}` + `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${ + currentOrgDomain?.replace("/", "") ?? "" + }` ); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index d6b21a118f..780286c47a 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -83,7 +83,7 @@ inferSSRProps & WithNonceProps<{}>) { const telemetry = useTelemetry(); - let callbackUrl = searchParams.get("callbackUrl") || ""; + let callbackUrl = searchParams?.get("callbackUrl") || ""; if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1); diff --git a/apps/web/pages/auth/oauth2/authorize.tsx b/apps/web/pages/auth/oauth2/authorize.tsx index 6b2c276aac..e34635540c 100644 --- a/apps/web/pages/auth/oauth2/authorize.tsx +++ b/apps/web/pages/auth/oauth2/authorize.tsx @@ -22,7 +22,7 @@ export default function Authorize() { const state = searchParams?.get("state") as string; const scope = searchParams?.get("scope") as string; - const queryString = searchParams.toString(); + const queryString = searchParams?.toString(); const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>(); const scopes = scope ? scope.toString().split(",") : []; diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx index 1fe903613f..4badbd1804 100644 --- a/apps/web/pages/auth/setup/index.tsx +++ b/apps/web/pages/auth/setup/index.tsx @@ -24,7 +24,7 @@ function useSetStep() { const searchParams = useSearchParams(); const pathname = usePathname(); const setStep = (newStep = 1) => { - const _searchParams = new URLSearchParams(searchParams); + const _searchParams = new URLSearchParams(searchParams ?? undefined); _searchParams.set("step", newStep.toString()); router.replace(`${pathname}?${_searchParams.toString()}`); }; diff --git a/apps/web/pages/auth/verify.tsx b/apps/web/pages/auth/verify.tsx index 8f6193d5ef..d0ce633d2f 100644 --- a/apps/web/pages/auth/verify.tsx +++ b/apps/web/pages/auth/verify.tsx @@ -164,7 +164,7 @@ export default function Verify() { e.preventDefault(); setSecondsLeft(30); // Update query params with t:timestamp, shallow: true doesn't re-render the page - const _searchParams = new URLSearchParams(searchParams.toString()); + const _searchParams = new URLSearchParams(searchParams?.toString()); _searchParams.set("t", `${Date.now()}`); router.replace(`${pathname}?${_searchParams.toString()}`); return await sendVerificationLogin(customer.email, customer.username); diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index af7b779dc8..44eb293a1a 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -155,7 +155,7 @@ export default function Success(props: SuccessProps) { const [calculatedDuration, setCalculatedDuration] = useState(undefined); const { requiresLoginToUpdate } = props; function setIsCancellationMode(value: boolean) { - const _searchParams = new URLSearchParams(searchParams); + const _searchParams = new URLSearchParams(searchParams ?? undefined); if (value) { _searchParams.set("cancel", "true"); diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 27d825aa70..bd87521314 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -298,7 +298,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL // inject selection data into url for correct router history const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => { - const newSearchParams = new URLSearchParams(searchParams); + const newSearchParams = new URLSearchParams(searchParams ?? undefined); function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) { if (value) newSearchParams.set(key, value.toString()); if (value === null) newSearchParams.delete(key); diff --git a/packages/app-store/alby/components/AlbyPaymentComponent.tsx b/packages/app-store/alby/components/AlbyPaymentComponent.tsx index a4977b64cd..34599a9800 100644 --- a/packages/app-store/alby/components/AlbyPaymentComponent.tsx +++ b/packages/app-store/alby/components/AlbyPaymentComponent.tsx @@ -134,7 +134,15 @@ function PaymentChecker(props: PaymentCheckerProps) { const bookingSuccessRedirect = useBookingSuccessRedirect(); const utils = trpc.useContext(); const { t } = useLocale(); + useEffect(() => { + if (searchParams === null) { + return; + } + + // use closure to ensure non-nullability + const sp = searchParams; + const interval = setInterval(() => { (async () => { if (props.booking.status === "ACCEPTED") { @@ -153,7 +161,7 @@ function PaymentChecker(props: PaymentCheckerProps) { location: string; } = { uid: props.booking.uid, - email: searchParams.get("email"), + email: sp.get("email"), location: t("web_conferencing_details_to_follow"), }; @@ -165,6 +173,7 @@ function PaymentChecker(props: PaymentCheckerProps) { } })(); }, 1000); + return () => clearInterval(interval); }, [ bookingSuccessRedirect, @@ -178,5 +187,6 @@ function PaymentChecker(props: PaymentCheckerProps) { t, utils.viewer.bookings, ]); + return null; } diff --git a/packages/app-store/alby/pages/setup/index.tsx b/packages/app-store/alby/pages/setup/index.tsx index 9017af73ec..fdd8403b03 100644 --- a/packages/app-store/alby/pages/setup/index.tsx +++ b/packages/app-store/alby/pages/setup/index.tsx @@ -30,15 +30,20 @@ export default function AlbySetup(props: IAlbySetupProps) { function AlbySetupCallback() { const [error, setError] = useState(null); - const params = useSearchParams(); + const searchParams = useSearchParams(); + useEffect(() => { + if (!searchParams) { + return; + } + if (!window.opener) { setError("Something went wrong. Opener not available. Please contact support@getalby.com"); return; } - const code = params.get("code"); - const error = params.get("error"); + const code = searchParams?.get("code"); + const error = searchParams?.get("error"); if (!code) { setError("declined"); @@ -54,7 +59,7 @@ function AlbySetupCallback() { payload: { code }, }); window.close(); - }, []); + }, [searchParams]); return (
diff --git a/packages/app-store/routing-forms/components/FormActions.tsx b/packages/app-store/routing-forms/components/FormActions.tsx index 17a2932d1c..6c96a221eb 100644 --- a/packages/app-store/routing-forms/components/FormActions.tsx +++ b/packages/app-store/routing-forms/components/FormActions.tsx @@ -48,7 +48,7 @@ export const useOpenModal = () => { const pathname = usePathname(); const searchParams = useSearchParams(); const openModal = (option: z.infer) => { - const newQuery = new URLSearchParams(searchParams); + const newQuery = new URLSearchParams(searchParams ?? undefined); newQuery.set("dialog", "new-form"); Object.keys(option).forEach((key) => { newQuery.set(key, option[key as keyof typeof option] || ""); diff --git a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx index 8ff57e60ea..18b400a72d 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx @@ -301,7 +301,7 @@ const usePrefilledResponse = (form: Props["form"]) => { // Prefill the form from query params form.fields?.forEach((field) => { - const valuesFromQuery = searchParams?.getAll(getFieldIdentifier(field)).filter(Boolean); + const valuesFromQuery = searchParams?.getAll(getFieldIdentifier(field)).filter(Boolean) ?? []; // We only want to keep arrays if the field is a multi-select const value = valuesFromQuery.length > 1 ? valuesFromQuery : valuesFromQuery[0]; diff --git a/packages/features/apps/AdminAppsList.tsx b/packages/features/apps/AdminAppsList.tsx index 5991687276..5c96da95bd 100644 --- a/packages/features/apps/AdminAppsList.tsx +++ b/packages/features/apps/AdminAppsList.tsx @@ -268,7 +268,7 @@ interface EditModalState extends Pick { const AdminAppsListContainer = () => { const searchParams = useSearchParams(); const { t } = useLocale(); - const category = searchParams.get("category") || AppCategories.calendar; + const category = searchParams?.get("category") || AppCategories.calendar; const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery( { category }, diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx index a4e52447f5..1e7e479bca 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarContainer.tsx @@ -38,7 +38,7 @@ function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) { // Toggle query param for overlay calendar const toggleOverlayCalendarQueryParam = useCallback( (state: boolean) => { - const current = new URLSearchParams(Array.from(searchParams.entries())); + const current = new URLSearchParams(Array.from(searchParams?.entries() ?? [])); if (state) { current.set("overlayCalendar", "true"); localStorage.setItem("overlayCalendarSwitchDefault", "true"); @@ -121,7 +121,7 @@ export function OverlayCalendarContainer() { const { data: session, status: sessionStatus } = useSession(); const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates); const switchEnabled = - searchParams.get("overlayCalendar") === "true" || + searchParams?.get("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault") === "true"; const selectedDate = useBookerStore((state) => state.selectedDate); diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index 519861837f..b32440197c 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -62,7 +62,7 @@ export const useScheduleForEvent = ({ shallow ); const searchParams = useSearchParams(); - const rescheduleUid = searchParams.get("rescheduleUid"); + const rescheduleUid = searchParams?.get("rescheduleUid"); const pathname = usePathname(); @@ -78,6 +78,6 @@ export const useScheduleForEvent = ({ rescheduleUid, month: monthFromStore ?? month, duration: durationFromStore ?? duration, - isTeamEvent: pathname.indexOf("/team/") !== -1 || isTeam, + isTeamEvent: pathname?.indexOf("/team/") !== -1 || isTeam, }); }; diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx index 85d8f22d2c..3101892eaa 100644 --- a/packages/features/bookings/components/event-meta/Members.tsx +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -61,7 +61,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined, alt: profile.name || undefined, href: profile.username - ? `${CAL_URL}${pathname.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}` + ? `${CAL_URL}${pathname?.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}` : undefined, }); diff --git a/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx index 0e026e8127..6575ddda81 100644 --- a/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx +++ b/packages/features/ee/organizations/pages/settings/other-team-members-view.tsx @@ -58,7 +58,7 @@ const MembersView = () => { const { t, i18n } = useLocale(); const router = useRouter(); const searchParams = useSearchParams(); - const teamId = Number(searchParams.get("id")); + const teamId = Number(searchParams?.get("id")); const session = useSession(); const utils = trpc.useContext(); const [offset, setOffset] = useState(1); @@ -74,6 +74,7 @@ const MembersView = () => { const { data: team, isLoading: isTeamLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( { teamId }, { + enabled: !Number.isNaN(teamId), onError: () => { router.push("/settings"); }, @@ -86,13 +87,14 @@ const MembersView = () => { distinctUser: true, }, { - enabled: searchParams !== null, + enabled: !Number.isNaN(teamId), } ); const { data: membersFetch, isLoading: isLoadingMembers } = trpc.viewer.organizations.listOtherTeamMembers.useQuery( { teamId, limit, offset: (offset - 1) * limit }, { + enabled: !Number.isNaN(teamId), onError: () => { router.push("/settings"); }, @@ -101,12 +103,8 @@ const MembersView = () => { useEffect(() => { if (membersFetch) { - if (membersFetch.length < limit) { - setLoadMore(false); - } else { - setLoadMore(true); - } - setMembers(members.concat(membersFetch)); + setLoadMore(membersFetch.length >= limit); + setMembers((m) => m.concat(membersFetch)); } }, [membersFetch]); diff --git a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx index 1d97111bbc..1b04688418 100644 --- a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx +++ b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx @@ -77,11 +77,11 @@ const OtherTeamProfileView = () => { resolver: zodResolver(teamProfileFormSchema), }); const searchParams = useSearchParams(); - const teamId = Number(searchParams.get("id")); + const teamId = Number(searchParams?.get("id")); const { data: team, isLoading } = trpc.viewer.organizations.getOtherTeam.useQuery( { teamId: teamId }, { - enabled: !!teamId, + enabled: !Number.isNaN(teamId), onError: () => { router.push("/settings"); }, diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index be4f3a5e69..4311e9036c 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -81,12 +81,17 @@ const PaymentForm = (props: Props) => { const handleSubmit = async (ev: SyntheticEvent) => { ev.preventDefault(); - if (!stripe || !elements) return; + if (!stripe || !elements || searchParams === null) { + return; + } + setState({ status: "processing" }); let payload; const params: { - [k: string]: any; + uid: string; + email: string | null; + location?: string; } = { uid: props.booking.uid, email: searchParams.get("email"), diff --git a/packages/features/ee/users/pages/users-add-view.tsx b/packages/features/ee/users/pages/users-add-view.tsx index 052e546443..30ffc93ae0 100644 --- a/packages/features/ee/users/pages/users-add-view.tsx +++ b/packages/features/ee/users/pages/users-add-view.tsx @@ -17,7 +17,10 @@ const UsersAddView = () => { onSuccess: async () => { showToast("User added successfully", "success"); await utils.viewer.users.list.invalidate(); - router.replace(pathname?.replace("/add", "")); + + if (pathname !== null) { + router.replace(pathname.replace("/add", "")); + } }, onError: (err) => { console.error(err.message); diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 8ed8abb4dd..69df6baaf6 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -520,18 +520,19 @@ const EmbedTypeCodeAndPreviewDialogContent = ({ (state) => [state.month, state.selectedDatesAndTimes], shallow ); - const eventId = searchParams.get("eventId"); + const eventId = searchParams?.get("eventId"); + const parsedEventId = parseInt(eventId ?? "", 10); const calLink = decodeURIComponent(embedUrl); const { data: eventTypeData } = trpc.viewer.eventTypes.get.useQuery( - { id: parseInt(eventId as string) }, - { enabled: !!eventId && embedType === "email", refetchOnWindowFocus: false } + { id: parsedEventId }, + { enabled: !Number.isNaN(parsedEventId) && embedType === "email", refetchOnWindowFocus: false } ); const s = (href: string) => { const _searchParams = new URLSearchParams(searchParams); const [a, b] = href.split("="); _searchParams.set(a, b); - return `${pathname?.split("?")[0]}?${_searchParams.toString()}`; + return `${pathname?.split("?")[0] ?? ""}?${_searchParams.toString()}`; }; const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) })); const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject> = {}; diff --git a/packages/features/insights/context/FiltersProvider.tsx b/packages/features/insights/context/FiltersProvider.tsx index dcebb2a777..2eec6e91b3 100644 --- a/packages/features/insights/context/FiltersProvider.tsx +++ b/packages/features/insights/context/FiltersProvider.tsx @@ -8,21 +8,22 @@ import { trpc } from "@calcom/trpc"; import type { FilterContextType } from "./provider"; import { FilterProvider } from "./provider"; +const querySchema = z.object({ + startTime: z.string().nullable(), + endTime: z.string().nullable(), + teamId: z.coerce.number().nullable(), + userId: z.coerce.number().nullable(), + memberUserId: z.coerce.number().nullable(), + eventTypeId: z.coerce.number().nullable(), + filter: z.enum(["event-type", "user"]).nullable(), +}); + export function FiltersProvider({ children }: { children: React.ReactNode }) { // searchParams to get initial values from query params const utils = trpc.useContext(); const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); - const querySchema = z.object({ - startTime: z.string().nullable(), - endTime: z.string().nullable(), - teamId: z.coerce.number().nullable(), - userId: z.coerce.number().nullable(), - memberUserId: z.coerce.number().nullable(), - eventTypeId: z.coerce.number().nullable(), - filter: z.enum(["event-type", "user"]).nullable(), - }); let startTimeParsed, endTimeParsed, @@ -33,13 +34,13 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) { memberUserIdParsed; const safe = querySchema.safeParse({ - startTime: searchParams.get("startTime"), - endTime: searchParams.get("endTime"), - teamId: searchParams.get("teamId"), - userId: searchParams.get("userId"), - eventTypeId: searchParams.get("eventTypeId"), - filter: searchParams.get("filter"), - memberUserId: searchParams.get("memberUserId"), + startTime: searchParams?.get("startTime") ?? null, + endTime: searchParams?.get("endTime") ?? null, + teamId: searchParams?.get("teamId") ?? null, + userId: searchParams?.get("userId") ?? null, + eventTypeId: searchParams?.get("eventTypeId") ?? null, + filter: searchParams?.get("filter") ?? null, + memberUserId: searchParams?.get("memberUserId") ?? null, }); if (!safe.success) { @@ -119,7 +120,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) { initialConfig, } = newConfigFilters; const [startTime, endTime] = dateRange || [null, null]; - const newSearchParams = new URLSearchParams(searchParams.toString()); + const newSearchParams = new URLSearchParams(searchParams?.toString() ?? undefined); function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) { if (value !== undefined && value !== null) newSearchParams.set(key, value.toString()); } diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 22eb8685cd..af669bd647 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -509,7 +509,7 @@ export type NavigationItemType = { }: { item: Pick; isChild?: boolean; - pathname: string; + pathname: string | null; }) => boolean; }; @@ -527,7 +527,7 @@ const navigation: NavigationItemType[] = [ href: "/bookings/upcoming", icon: Calendar, badge: , - isCurrent: ({ pathname }) => pathname?.startsWith("/bookings"), + isCurrent: ({ pathname }) => pathname?.startsWith("/bookings") ?? false, }, { name: "availability", @@ -547,7 +547,7 @@ const navigation: NavigationItemType[] = [ icon: Grid, isCurrent: ({ pathname: path, item }) => { // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) - return path?.startsWith(item.href) && !path?.includes("routing-forms/"); + return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false); }, child: [ { @@ -556,7 +556,9 @@ const navigation: NavigationItemType[] = [ isCurrent: ({ pathname: path, item }) => { // During Server rendering path is /v2/apps but on client it becomes /apps(weird..) return ( - path?.startsWith(item.href) && !path?.includes("routing-forms/") && !path?.includes("/installed") + (path?.startsWith(item.href) ?? false) && + !(path?.includes("routing-forms/") ?? false) && + !(path?.includes("/installed") ?? false) ); }, }, @@ -564,7 +566,8 @@ const navigation: NavigationItemType[] = [ name: "installed_apps", href: "/apps/installed/calendar", isCurrent: ({ pathname: path }) => - path?.startsWith("/apps/installed/") || path?.startsWith("/v2/apps/installed/"), + (path?.startsWith("/apps/installed/") ?? false) || + (path?.startsWith("/v2/apps/installed/") ?? false), }, ], }, @@ -577,7 +580,7 @@ const navigation: NavigationItemType[] = [ name: "Routing Forms", href: "/apps/routing-forms/forms", icon: FileText, - isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/"), + isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false, }, { name: "workflows", @@ -631,7 +634,7 @@ function useShouldDisplayNavigationItem(item: NavigationItemType) { } const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, pathname }) => { - return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) : false; + return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) ?? false : false; }; const NavigationItem: React.FC<{ diff --git a/packages/features/webhooks/pages/webhook-edit-view.tsx b/packages/features/webhooks/pages/webhook-edit-view.tsx index 7b3c62a857..e44ffeb5e6 100644 --- a/packages/features/webhooks/pages/webhook-edit-view.tsx +++ b/packages/features/webhooks/pages/webhook-edit-view.tsx @@ -12,7 +12,7 @@ import { subscriberUrlReserved } from "../lib/subscriberUrlReserved"; const EditWebhook = () => { const searchParams = useSearchParams(); - const id = searchParams.get("id"); + const id = searchParams?.get("id"); if (!id) return ; diff --git a/packages/lib/bookingSuccessRedirect.ts b/packages/lib/bookingSuccessRedirect.ts index 303f4fca4e..b1e3c6a8a0 100644 --- a/packages/lib/bookingSuccessRedirect.ts +++ b/packages/lib/bookingSuccessRedirect.ts @@ -60,7 +60,7 @@ export const useBookingSuccessRedirect = () => { ...query, ...bookingExtraParams, }, - searchParams, + searchParams: searchParams ?? undefined, }); window.parent.location.href = `${url.toString()}?${newSearchParams.toString()}`; return; From 64d634e4068ab89aeef0770ef34c0e4913692d16 Mon Sep 17 00:00:00 2001 From: Greg Pabian <35925521+grzpab@users.noreply.github.com> Date: Sat, 21 Oct 2023 01:48:20 +0200 Subject: [PATCH 026/118] chore: [app dir bootstrapping 1] generate nonce with native crypto API (#11969) --- apps/web/lib/buildNonce.test.ts | 96 +++++++++++++++++++++++++++++++++ apps/web/lib/buildNonce.ts | 46 ++++++++++++++++ apps/web/lib/csp.ts | 5 +- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 apps/web/lib/buildNonce.test.ts create mode 100644 apps/web/lib/buildNonce.ts diff --git a/apps/web/lib/buildNonce.test.ts b/apps/web/lib/buildNonce.test.ts new file mode 100644 index 0000000000..46c7f6c26e --- /dev/null +++ b/apps/web/lib/buildNonce.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; + +import { buildNonce } from "./buildNonce"; + +describe("buildNonce", () => { + it("should return an empty string for an empty array", () => { + const nonce = buildNonce(new Uint8Array()); + + expect(nonce).toEqual(""); + expect(atob(nonce).length).toEqual(0); + }); + + it("should return a base64 string for values from 0 to 63", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 64 to 127", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 64); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 128 to 191", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 128); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 192 to 255", () => { + const array = Array(22) + .fill(0) + .map((_, i) => i + 192); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ABCDEFGHIJKLMNOPQRSTQQ=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for values from 0 to 42", () => { + const array = Array(22) + .fill(0) + .map((_, i) => 2 * i); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("ACEGIKMOQSUWYacegikmgg=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for 0 values", () => { + const array = Array(22) + .fill(0) + .map(() => 0); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("AAAAAAAAAAAAAAAAAAAAAA=="); + + expect(atob(nonce).length).toEqual(16); + }); + + it("should return a base64 string for 0xFF values", () => { + const array = Array(22) + .fill(0) + .map(() => 0xff); + const nonce = buildNonce(new Uint8Array(array)); + + expect(nonce.length).toEqual(24); + expect(nonce).toEqual("////////////////////ww=="); + + expect(atob(nonce).length).toEqual(16); + }); +}); diff --git a/apps/web/lib/buildNonce.ts b/apps/web/lib/buildNonce.ts new file mode 100644 index 0000000000..211371dbc7 --- /dev/null +++ b/apps/web/lib/buildNonce.ts @@ -0,0 +1,46 @@ +const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/* +The buildNonce array allows a randomly generated 22-unsigned-byte array +and returns a 24-ASCII character string that mimics a base64-string. +*/ + +export const buildNonce = (uint8array: Uint8Array): string => { + // the random uint8array should contain 22 bytes + // 22 bytes mimic the base64-encoded 16 bytes + // base64 encodes 6 bits (log2(64)) with 8 bits (64 allowed characters) + // thus ceil(16*8/6) gives us 22 bytes + if (uint8array.length != 22) { + return ""; + } + + // for each random byte, we take: + // a) only the last 6 bits (so we map them to the base64 alphabet) + // b) for the last byte, we are interested in two bits + // explaination: + // 16*8 bits = 128 bits of information (order: left->right) + // 22*6 bits = 132 bits (order: left->right) + // thus the last byte has 4 redundant (least-significant, right-most) bits + // it leaves the last byte with 2 bits of information before the redundant bits + // so the bitmask is 0x110000 (2 bits of information, 4 redundant bits) + const bytes = uint8array.map((value, i) => { + if (i < 20) { + return value & 0b111111; + } + + return value & 0b110000; + }); + + const nonceCharacters: string[] = []; + + bytes.forEach((value) => { + nonceCharacters.push(BASE64_ALPHABET.charAt(value)); + }); + + // base64-encoded strings can be padded with 1 or 2 `=` + // since 22 % 4 = 2, we pad with two `=` + nonceCharacters.push("=="); + + // the end result has 22 information and 2 padding ASCII characters = 24 ASCII characters + return nonceCharacters.join(""); +}; diff --git a/apps/web/lib/csp.ts b/apps/web/lib/csp.ts index 830ad7ffff..257f0d2dc7 100644 --- a/apps/web/lib/csp.ts +++ b/apps/web/lib/csp.ts @@ -1,10 +1,11 @@ -import crypto from "crypto"; import type { IncomingMessage, OutgoingMessage } from "http"; import { z } from "zod"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { buildNonce } from "@lib/buildNonce"; + function getCspPolicy(nonce: string) { //TODO: Do we need to explicitly define it in turbo.json const CSP_POLICY = process.env.CSP_POLICY; @@ -59,7 +60,7 @@ export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) { } const CSP_POLICY = process.env.CSP_POLICY; const cspEnabledForInstance = CSP_POLICY; - const nonce = crypto.randomBytes(16).toString("base64"); + const nonce = buildNonce(crypto.getRandomValues(new Uint8Array(22))); const parsedUrl = new URL(req.url, "http://base_url"); const cspEnabledForPage = cspEnabledForInstance && isPagePathRequest(parsedUrl); From 2a8f7412dd796d87a7f491d8f2bff63cbc2dd5c5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 20 Oct 2023 23:51:24 +0000 Subject: [PATCH 027/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/eu/common.json | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 1c30877c19..1c248ba252 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -315,6 +315,7 @@ "password_updated_successfully": "Pasahitza egoki eguneratu da", "password_has_been_changed": "Zure pasahitza egoki aldatu da.", "error_changing_password": "Errorea pasahitza aldatzean", + "session_timeout_change_error": "Errorea saioaren konfigurazioa eguneratzerakoan", "something_went_wrong": "Zerbait gaizki joan da.", "something_doesnt_look_right": "Zerbaitek ez du itxura onik?", "please_try_again": "Saia zaitez berriro, mesedez.", @@ -331,6 +332,35 @@ "password_hint_admin_min": "Gutxienez 15 karaktereko luzera", "password_hint_num": "Gutxienez zenbaki bat", "max_limit_allowed_hint": "{{limit}} karaktere edo gutxiagoko luzera izan behar du", + "invalid_password_hint": "Pasahitzak gutxienez {{passwordLength}} karaktereko luzera behar du gutxienez zenbaki bat eta letra maiuskula zein minuskulak nahasten dituela", + "incorrect_password": "Pasahitza ez da zuzena.", + "incorrect_email_password": "Emaila edo pasahitza ez dira zuzenak.", + "am_pm": "am/pm", + "january": "Urtarrila", + "february": "Otsaila", + "march": "Martxoa", + "april": "Apirila", + "may": "Maiatza", + "june": "Ekaina", + "july": "Uztaila", + "august": "Abuztua", + "september": "Iraila", + "october": "Urria", + "november": "Azaroa", + "december": "Abendua", + "monday": "Astelehena", + "tuesday": "Asteartea", + "wednesday": "Asteazkena", + "thursday": "Osteguna", + "friday": "Ostirala", + "saturday": "Larunbata", + "sunday": "Igandea", + "all_booked_today": "Dena erreserbatuta.", + "additional_guests": "Gehitu gonbidatuak", + "your_name": "Zure izena", + "your_full_name": "Zure izen osoa", + "no_name": "Izenik ez", + "enter_number_between_range": "Mesedez sartu 1 eta {{maxOccurences}} arteko zenbaki bat", "email_address": "Email helbidea", "enter_valid_email": "Mesedez, adierazi baliozko email helbide bat", "location": "Kokapena", @@ -352,15 +382,385 @@ "or": "EDO", "go_back": "Atzera", "email_or_username": "Emaila edo erabiltzaile izena", + "send_invite_email": "Bidali gonbidapen-email bat", + "role": "Eginkizuna", + "edit_role": "Editatu eginkizuna", + "edit_team": "Editatu taldea", + "reject": "Baztertu", + "reject_all": "Baztertu guztiak", + "accept": "Onartu", + "profile": "Profila", + "my_team_url": "Nire taldearen URLa", + "my_teams": "Nire taldeak", + "team_name": "Taldearen izena", + "your_team_name": "Zure taldearen izena", + "team_updated_successfully": "Taldea egoki eguneratu da", + "your_team_updated_successfully": "Zure taldea egoki eguneratu da.", + "your_org_updated_successfully": "Zure erakundea egoki eguneratu da.", + "about": "Honi buruz", + "team_description": "Esaldi gutxi batzuk zure taldeari buruz. Zure taldearen orrialdean agertuko dira.", + "org_description": "Esaldi gutxi batzuk zure erakundeari buruz. Zure erakundearen orrialdean agertuko dira.", + "members": "Kideak", + "organization_members": "Erakundeko kideak", + "member": "Kidea", + "number_member_one": "{{count}} kide", + "danger_zone": "Arrisku gunea", + "account_deletion_cannot_be_undone": "Kontuz. Kontuak ezabatzea ezin da desegin.", + "back": "Atzera", + "cancel_event": "Bertan behera utzi gertaera", + "continue": "Jarraitu", + "confirm": "Baieztatu", + "confirm_all": "Baieztatu guztiak", + "confirm_remove_member": "Bai, ezabatu kidea", + "remove_member": "Ezabatu kidea", + "manage_your_team": "Kudeatu zure taldea", + "no_teams": "Oraindik ez daukazu talderik.", + "submit": "Bidali", + "delete": "Ezabatu", + "update": "Eguneratu", + "save": "Gorde", + "pending": "Egiteke", + "open_options": "Ireki aukerak", + "copy_link": "Kopiatu gertaerarako esteka", + "share": "Partekatu", + "copy_link_team": "Kopiatu talderako esteka", + "leave_team": "Utzi taldea", + "confirm_leave_team": "Bai, utzi taldea", + "leave_team_confirmation_message": "Ziur al zaude talde hau utzi nahi duzula? Ezingo duzu erreserbarik egin taldea erabiliz hemendik aurrera.", + "preview": "Aurreikusi", + "link_copied": "Esteka kopiatuta!", + "private_link_copied": "Esteka pribatua kopiatuta!", + "link_shared": "Esteka partekatuta!", + "title": "Izenburua", + "description": "Deskribapena", + "preview_team": "Aurreikusi taldea", + "duration": "Iraupena", + "available_durations": "Iraupen aukerak", + "default_duration": "Lehenetsitako iraupena", + "minutes": "minutu", + "username_placeholder": "erabiltzaile izena", + "count_members_one": "kide {{count}}", + "count_members_other": "{{count}} kide", + "url": "URLa", + "hidden": "Ezkutuan", + "readonly": "Irakurtzeko bakarrik", + "one_time_link": "Aldi bakarreko esteka", + "upload_avatar": "Kargatu abatarra", + "language": "Hizkuntza", + "timezone": "Ordu-eremua", + "first_day_of_week": "Asteko lehen eguna", + "plus_more": "{{count}} gehiago", + "create_team": "Sortu taldea", + "name": "Izena", + "create_new_team_description": "Sortu talde berri bat erabiltzaileekin elkarlanean aritzeko.", + "create_new_team": "Sortu talde berri bat", + "open_invitations": "Gonbidapen irekiak", + "new_team": "Talde berria", + "create_first_team_and_invite_others": "Sortu zure lehen taldea eta gonbidatu beste erabiltzaileak elkarlanean aritzera.", + "create_team_to_get_started": "Sortu talde bat hasteko", + "teams": "Taldeak", + "team": "Taldea", + "organization": "Erakundea", + "change_email_tip": "Saioa itxi eta berriro hasi beharko duzu aldaketa ikusi ahal izateko.", + "little_something_about": "Kontatu zuri buruzko zerbait.", + "profile_updated_successfully": "Profila egoki eguneratu da", + "your_user_profile_updated_successfully": "Zure erabiltzaile profila egoki eguneratu da.", + "enabled": "Gaituta", + "disabled": "Ezgaituta", + "disable": "Ezgaitu", + "billing": "Fakturazioa", + "manage_your_billing_info": "Kudeatu zure fakturaziorako informazioa eta amaitu zure harpidetza.", + "logo": "Logoa", + "error": "Errorea", + "team_logo": "Taldearen logoa", + "add_location": "Gehitu kokapena", + "attendees": "Partaideak", + "add_attendees": "Gehitu partaideak", + "label": "Etiketa", + "type": "Mota", + "edit": "Editatu", + "disable_notes": "Ezkutatu oharrak egutegian", + "recurring_event": "Gertaera errepikaria", + "disable_guests": "Ezgaitu gonbidatuak", + "private_link": "Sortu esteka pribatua", + "enable_private_url": "Gaitu URL pribatua", + "private_link_label": "Esteka pribatua", + "private_link_hint": "Zure esteka pribatua birsortu egingo da erabilera bakoitzaren ondoren", + "copy_private_link": "Kopiatu esteka pribatua", + "invitees_can_schedule": "Gobnidatuek programatu dezakete", + "set_address_place": "Ezarri helbide edo toki bat", + "set_link_meeting": "Ezarri esteka bat bilerarako", + "you_need_to_add_a_name": "Izena gehitu behar duzu", + "hide_event_type": "Ezkutatu gertaera mota", + "edit_location": "Editatu kokapena", + "quick_chat": "Elkarrizketa azkarra", + "add_new_event_type": "Gehitu gertaera mota berri bat", + "length": "Luzera", + "delete_event_type": "Ezabatu gertaera mota?", + "confirm_delete_event_type": "Bai, ezabatu", + "delete_account": "Ezabatu kontua", + "confirm_delete_account": "Bai, ezabatu kontua", + "settings": "Ezarpenak", + "event_type_moved_successfully": "Gertaera mota zuzen mugitu da", + "next_step_text": "Hurrengo pausoa", + "next_step": "Saltatu pausoa", + "prev_step": "Aurreko pausoa", + "install": "Instalatu", + "installed": "Instalatua", + "disconnect": "Deskonektatu", + "automation": "Automatizazioa", + "connect_additional_calendar": "Konektatu egutegi bat gehiago", + "calendar_updated_successfully": "Egutegia zuzen eguneratu da", + "calendar": "Egutegia", + "payments": "Ordainketak", + "not_installed": "Instalatu gabe", + "error_password_mismatch": "Pasahitzak ez datoz bat.", + "error_required_field": "Eremu hau nahitaezkoa da.", + "status": "Egoera", + "signin_with_google": "Hasi saioa Googlerekin", + "signin_with_saml": "Hasi saioa SAMLrekin", + "signin_with_saml_oidc": "Hasi saioa SAML/OIDCrekin", + "import": "Inportatu", + "import_from": "Inportatu hemendik:", + "featured_categories": "Nabarmendutako kategoriak", + "popular_categories": "Kategoria ospetsuak", + "most_popular": "Ospetsuenak", + "permissions": "Baimenak", + "terms_and_privacy": "Baldintzak eta pribatutasuna", + "subscribe": "Harpidetu", + "buy": "Erosi", + "categories": "Kategoriak", + "pricing": "Prezioak", + "learn_more": "Gehiago ikasi", + "privacy_policy": "Pribatutasun politika", + "terms_of_service": "Erabilera-baldintzak", + "remove": "Ezabatu", + "add": "Gehitu", + "installed_other": "{{count}} instalatuta", + "next_steps": "Hurrengo pausoak", + "error_404": "404 errorea", + "default": "Lehenetsitakoa", + "set_to_default": "Ezarri lehenetsitako gisa", + "new_schedule_btn": "Programazio berria", + "add_new_schedule": "Gehitu programazio berria", + "add_new_calendar": "Gehitu egutegi berria", + "delete_schedule": "Ezabatu programazioa", + "default_schedule_name": "Lanorduak", + "example_name": "Mikel Biteri", + "time_format": "Ordu-formatua", + "12_hour": "12 ordu", + "24_hour": "24 ordu", + "12_hour_short": "12o", + "24_hour_short": "24o", + "redirect_success_booking": "Birbideratu erreserbatzean ", + "create": "Sortu", + "copy_to_clipboard": "Kopiatu arbelera", + "copy": "Kopiatu", + "request_reschedule_booking": "Eskatu zure erreserba berrantolatzeko", + "reason_for_reschedule": "Berrantolatzeko arrazoia", + "book_a_new_time": "Erreserbatu momentu berri bat", + "reschedule_request_sent": "Berrantolatzeko eskaera bidalita", + "reschedule_modal_description": "Honek programatutako bilera bertan behera utziko du, programatzaileari jakinaraziko dio eta momentu berri bat hautatzeko eskatu.", + "reason_for_reschedule_request": "Berrantolatzea eskatzeko arrazoia", + "send_reschedule_request": "Berrantolatzeko eskatu ", "edit_booking": "Aldatu erreserba", "reschedule_booking": "Aldatu erreserbaren programazioa", + "former_time": "Lehengo ordua", + "confirmation_page_gif": "Gehitu GIF bat zure baieztapen-orrialdera", + "search": "Bilatu", + "make_team_private": "Bihurtu taldea pribatua", "location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an", + "current_location": "Uneko kokapena", + "new_location": "Kokapen berria", + "session": "Saioa", + "session_description": "Kontrolatu zure kontuaren saioa", + "no_location": "Ez dago kokapenik definituta", + "set_location": "Ezarri kokapena", + "update_location": "Eguneratu kokapena", + "location_updated": "Kokapena eguneratuta", + "email_validation_error": "Honek ez du email helbide baten itxurarik", + "copy_code": "Kopiatu kodea", + "code_copied": "Kodea kopiatuta!", + "calendar_url": "Egutegiaren URLa", + "set_your_phone_number": "Ezarri telefono zenbaki bat bilerarako", + "display_location_label": "Erakutsi erreserba orrialdean", + "display_location_info_badge": "Kokapena ikusgarri egongo da erreserba baieztatu aurretik", + "add_gif": "Gehitu GIF bat", + "search_giphy": "Bilatu Giphyn", + "add_link_from_giphy": "Gehitu Giphyko esteka bat", + "add_gif_to_confirmation": "Zure baieztapen-orrialdera GIF bat gehitzen", + "find_gif_spice_confirmation": "Aurkitu GIF bat zure baieztapen-orrialdea alaitzeko", + "resources": "Baliabideak", + "support_documentation": "Laguntzako dokumentazioa", + "developer_documentation": "Garatzaileentzako dokumentazioa", + "get_in_touch": "Jar zaitez harremanetan", "booking_details": "Erreserbaren xehetasunak", + "or_lowercase": "edo", + "go_to": "Joan hona: ", + "event_location": "Gertaeraren kokapena", + "reschedule_optional": "Berrantolatzeko arrazoia (aukerakoa)", + "reschedule_placeholder": "Jakinarazi besteei zergatik behar duzun berrantolatzea", + "event_cancelled": "Gertaera hau bertan behera geratu da", + "emailed_information_about_cancelled_event": "Email bat bidali diegu guztiei jakinaren gainean egon daitezen.", + "meeting_url_in_confirmation_email": "Bileraren URLa baieztapen emailean dago", + "url_start_with_https": "URLak http:// edo https:// hasi behar du", + "number_provided": "Telefono zenbakia emango da", + "before_event_trigger": "gertaera hasi aurretik", + "event_cancelled_trigger": "gertaera bertan behera geratzen denean", + "new_event_trigger": "gertaera berri bat erreserbatzen denean", + "email_host_action": "bidali emaila anfitrioiari", + "email_attendee_action": "bidali emaila partaideei", + "sms_attendee_action": "Bidali SMSa partaideari", + "sms_number_action": "bidali SMSa zenbaki jakin batera", + "whatsapp_number_action": "bidali Whatsapp mezua zenbaki jakin batera", + "whatsapp_attendee_action": "bidali Whatsapp mezua partaideari", + "reschedule_event_trigger": "gertaera berrantolatzen denean", + "day_timeUnit": "egun", + "hour_timeUnit": "ordu", + "minute_timeUnit": "minutu", + "current": "Unekoa", + "confirm_username_change_dialog_title": "Baieztatu erabiltzaile-izenaren aldaketa", + "requires_confirmation": "Baieztapena behar du", + "always_requires_confirmation": "Beti", + "email_body": "Emailaren gorputza", + "text_message": "Testu mezua", + "choose_template": "Hautatu txantiloi bat", + "reminder": "Gogorarazlea", + "rescheduled": "Berrantolatua", + "completed": "Osatuta", "reminder_email": "Gogorarazpena: {{eventType}} {{name}}(r)ekin {{date}}(e)an", + "minute_one": "minutu {{count}}", + "minute_other": "{{count}} minutu", + "hour_one": "ordu {{count}}", + "hour_other": "{{count}} ordu", + "attendee_name": "Partaidearen izena", + "scheduler_full_name": "Erreserba egiten duen pertsonaren izen osoa", + "no_active_event_types": "Ez dago gertaera mota aktiborik", "new_seat_subject": "{{name}} parte-hartzaile berria {{eventType}}(e)n {{date}}(e)an", + "new_seat_title": "Norbaitek bere burua gehitu du gertaera batera", + "variable": "Aldagaia", + "event_name_variable": "Gertaeraren izena", + "attendee_name_variable": "Partaidea", + "event_date_variable": "Gertaeraren data", + "event_time_variable": "Gertaeraren ordua", + "timezone_variable": "Ordu-eremua", + "location_variable": "Kokapena", + "additional_notes_variable": "Ohar gehigarriak", + "organizer_name_variable": "Antolatzailearen izena", + "invalid_number": "Telefono zenbaki baliogabea", + "navigate": "Nabigatu", + "open": "Ireki", + "close": "Itxi", + "upgrade": "Bertsio-berritu", + "upgrade_to_access_recordings_title": "Bertsio-berritu grabaketetarako sarbidea izateko", + "show_eventtype_on_profile": "Erakutsi profilean", + "new_username": "Erabiltzaile izen berria", + "current_username": "Uneko erabiltzaile izena", + "example_1": "1. adibidea", + "example_2": "2. adibidea", + "company_size": "Enpresaren tamaina", + "what_help_needed": "Zerekin behar duzu laguntza?", + "notification_sent": "Jakinarazpena bidalita", + "event_advanced_tab_title": "Aurreratua", + "do_this": "Egin honakoa", + "turn_off": "Itzali", + "turn_on": "Piztu", + "settings_updated_successfully": "Ezarpenak egoki eguneratu dira", + "error_updating_settings": "Errorea gertatu da ezarpenak eguneratzerakoan", + "bio_hint": "Esaldi gutxi batzuk zeuri buruz. Zure orrialde pertsonalean agertuko dira.", + "user_has_no_bio": "Erabiltzaile honek ez du bio bat gehitu oraindik.", + "bio": "Bio", + "delete_account_modal_title": "Ezabatu kontua", + "delete_my_account": "Ezabatu nire kontua", + "start_of_week": "Astearen hasiera", + "recordings_title": "Grabaketak", + "recording": "Grabaketa", + "happy_scheduling": "Programazio zoriontsua", + "select_calendars": "Hautatu zein egutegitan egiaztatu nahi duzun talkarik ote dagoen, erreserba bikoitzak saihesteko.", + "check_for_conflicts": "Egiaztatu talkak", + "view_recordings": "Grabaketak ikusi", + "adding_events_to": "Gertaerak hona gehitzen:", + "pro": "Pro", + "profile_picture": "Profileko irudia", + "upload": "Kargatu", + "add_profile_photo": "Gehitu profileko argazkia", + "web3": "Web3", + "old_password": "Pasahitz zaharra", + "secure_password": "Zure pasahitz berri super segurua", + "error_updating_password": "Errorea pasahitza eguneratzean", + "today": "gaur", + "appearance": "Itxura", + "my_account": "Nire kontua", + "general": "Orokorra", + "calendars": "Egutegiak", + "invoices": "Fakturak", + "users": "Erabiltzaileak", + "user": "Erabiltzailea", + "users_description": "Hemen erabiltzaile guztien zerrenda aurkituko duzu", + "add_variable": "Gehitu aldagaia", + "message_template": "Mezuen txantiloia", + "email_subject": "Emailaren gaia", + "event_name_info": "Gertaeraren motaren izena", + "event_date_info": "Gertaeraren data", + "event_time_info": "Gertaeraren hasiera-ordua", + "location_info": "Gertaeraren kokapena", + "additional_notes_info": "Erreserbaren ohar gehigarriak", + "organizer_name_info": "Antolatzailearen izena", + "download_responses": "Deskargatu erantzunak", + "download": "Deskargatu", + "download_recording": "Deskargatu grabaketa", + "create_your_first_form": "Sortu zure lehen galdetegia", + "profile_team_description": "Kudeatu zure talde-profilaren ezarpenak", + "profile_org_description": "Kudeatu zure erakunde-profilaren ezarpenak", + "members_team_description": "Talde honetan diren erabiltzaileak", + "organization_description": "Kudeatu zure erakundeko administrari eta kideak", + "team_url": "Taldearen URLa", + "team_members": "Taldekideak", + "more": "Gehiago", + "workflow_example_1": "Bidali SMS gogorarazlea gertaera hasi baino 24 ordu lehenago partaideari", + "workflow_example_4": "Bidali email gogorarazlea gertaerak hasi baino ordubete lehenago partaideari", + "edit_form_later_subtitle": "Geroago editatu ahal izango duzu.", + "connect_calendar_later": "Geroago konektatuko dut nire egutegia", "booking_appearance": "Erreserba-orriaren itxura", + "add_a_team": "Gehitu taldea", + "password_updated": "Pasahitza eguneratuta!", + "pending_payment": "Ordainketa egiteke", + "pending_invites": "Zain dauden gonbidapenak", + "no_calendar_installed": "Ez dago egutegirik instalatuta", + "no_calendar_installed_description": "Oraindik ez duzu zure egutegietako bat ere konektatu", + "add_a_calendar": "Gehitu egutegia", + "change_email_hint": "Saioa itxi eta berriro hasi beharko duzu edozein aldaketa ikusi ahal izateko", + "confirm_password_change_email": "Mesedez, baieztatu zure pasahitza, email helbidea aldatu aurretik", + "seats": "eserleku", "limit_booking_frequency": "Mugatu erreserbatzeko maiztasuna", + "calendar_connection_fail": "Egutegia konektatzeak huts egin du", + "booking_confirmation_success": "Erreserba egoki baieztatu da", + "booking_rejection_success": "Erreserba baztertzea zuzen egin da", "booking_tentative": "Erreserba hau behin-behinekoa da", + "booking_accept_intent": "Iepa, onartu egin nahi dut", + "we_wont_show_again": "Ez dugu hau berriro erakutsiko", + "couldnt_update_timezone": "Ezin izan dugu ordu-eremua eguneratu", + "updated_timezone_to": "Ordu-eremua eguneratua honakora: {{formattedCurrentTz}}", + "update_timezone": "Eguneratu ordu-eremua", + "update_timezone_question": "Eguneratu ordu-eremua?", + "dont_update": "Ez eguneratu", + "email_address_action": "bidali emaila email helbide jakin batera", + "after_event_trigger": "gertaera bukatu ondoren", + "how_long_after": "Gertaera bukatzen denetik zenbat denborara?", + "add_calendar": "Gehitu egutegia", "limit_future_bookings": "Mugatu etorkizuneko erreserbak", + "no_event_types": "Ez dago gertaera motarik ezarrita", + "no_event_types_description": "{{name}}(e)k ez du erreserbatu dezakezun gertaera motarik ezarri.", + "error_creating_team": "Errorea taldea sortzean", + "you": "Zu", + "resend_email": "Berbidali emaila", + "member_already_invited": "Kidea gonbidatua izan da lehendik ere", + "enter_email_or_username": "Sartu email edo erabiltzaile izen bat", + "team_name_taken": "Izen hau dagoeneko hartua dago", + "must_enter_team_name": "Taldearentzat izen bat behar da", + "fill_this_field": "Mesedez, bete ezazu eremu hau", + "options": "Aukerak", + "add_an_option": "Gehitu aukera bat", + "radio": "Irratia", "all_bookings_filter_label": "Erreserba guztiak" } From e91fe12219f2a4237d5ebd96c3569f23b5eea83b Mon Sep 17 00:00:00 2001 From: Greg Pabian <35925521+grzpab@users.noreply.github.com> Date: Sat, 21 Oct 2023 01:57:13 +0200 Subject: [PATCH 028/118] chore: [app dir bootstrapping 2] ensure tests do not have explicit timeouts (#11970) --- apps/web/playwright/booking-pages.e2e.ts | 2 +- apps/web/playwright/lib/testUtils.ts | 44 ++++++++++--------- .../manage-booking-questions.e2e.ts | 19 +++----- apps/web/playwright/webhook.e2e.ts | 40 ++++++----------- 4 files changed, 43 insertions(+), 62 deletions(-) diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 87ac1dcf51..63071b8f64 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -53,7 +53,7 @@ test.describe("free user", () => { // book same time spot again await bookTimeSlot(page); - await expect(page.locator("[data-testid=booking-fail]")).toBeVisible({ timeout: 1000 }); + await page.locator("[data-testid=booking-fail]").waitFor({ state: "visible" }); }); }); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index f401dca0f9..b9cf3850d6 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -1,5 +1,6 @@ import type { Frame, Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import EventEmitter from "events"; import type { IncomingMessage, ServerResponse } from "http"; import { createServer } from "http"; // eslint-disable-next-line no-restricted-imports @@ -35,7 +36,27 @@ export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) res.end(); }, } = opts; + const eventEmitter = new EventEmitter(); const requestList: Request[] = []; + + const waitForRequestCount = (count: number) => + new Promise((resolve) => { + if (requestList.length === count) { + resolve(); + return; + } + + const pushHandler = () => { + if (requestList.length !== count) { + return; + } + eventEmitter.off("push", pushHandler); + resolve(); + }; + + eventEmitter.on("push", pushHandler); + }); + const server = createServer((req, res) => { const buffer: unknown[] = []; @@ -49,6 +70,7 @@ export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) _req.body = json; requestList.push(_req); + eventEmitter.emit("push"); requestHandler({ req: _req, res }); }); }); @@ -58,34 +80,16 @@ export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) // eslint-disable-next-line @typescript-eslint/no-explicit-any const port: number = (server.address() as any).port; const url = `http://localhost:${port}`; + return { port, close: () => server.close(), requestList, url, + waitForRequestCount, }; } -/** - * When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass. - */ -export async function waitFor(fn: () => Promise | unknown, opts: { timeout?: number } = {}) { - let finished = false; - const timeout = opts.timeout ?? 5000; // 5s - const timeStart = Date.now(); - while (!finished) { - try { - await fn(); - finished = true; - } catch { - if (Date.now() - timeStart >= timeout) { - throw new Error("waitFor timed out"); - } - await new Promise((resolve) => setTimeout(resolve, 0)); - } - } -} - export async function selectFirstAvailableTimeSlotNextMonth(page: Page | Frame) { // Let current month dates fully render. await page.click('[data-testid="incrementMonth"]'); diff --git a/apps/web/playwright/manage-booking-questions.e2e.ts b/apps/web/playwright/manage-booking-questions.e2e.ts index 4091acfa1a..9f0d4762ae 100644 --- a/apps/web/playwright/manage-booking-questions.e2e.ts +++ b/apps/web/playwright/manage-booking-questions.e2e.ts @@ -8,7 +8,7 @@ import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { test } from "./lib/fixtures"; -import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; +import { createHttpServer, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; async function getLabelText(field: Locator) { return await field.locator("label").first().locator("span").first().innerText(); @@ -215,13 +215,7 @@ test.describe("Manage Booking Questions", () => { async function runTestStepsCommonForTeamAndUserEventType( page: Page, context: PlaywrightTestArgs["context"], - webhookReceiver: { - port: number; - close: () => import("http").Server; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestList: (import("http").IncomingMessage & { body?: any })[]; - url: string; - } + webhookReceiver: Awaited> ) { await page.click('[href$="tabName=advanced"]'); @@ -311,12 +305,11 @@ async function runTestStepsCommonForTeamAndUserEventType( await page.locator('[data-testid="field-response"][data-fob-field="how-are-you"]').innerText() ).toBe("I am great!"); - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); const [request] = webhookReceiver.requestList; + // @ts-expect-error body is unknown const payload = request.body.payload; expect(payload.responses).toMatchObject({ @@ -667,9 +660,7 @@ async function expectWebhookToBeCalled( }; } ) { - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); const [request] = webhookReceiver.requestList; const body = request.body; diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index d5e1d5b512..074ffbfd5c 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -10,7 +10,6 @@ import { bookOptinEvent, createHttpServer, selectFirstAvailableTimeSlotNextMonth, - waitFor, gotoRoutingLink, createUserWithSeatedEventAndAttendees, } from "./lib/testUtils"; @@ -78,10 +77,7 @@ test.describe("BOOKING_CREATED", async () => { await page.fill('[name="email"]', "test@example.com"); await page.press('[name="email"]', "Enter"); - // --- check that webhook was called - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); const [request] = webhookReceiver.requestList; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -209,10 +205,8 @@ test.describe("BOOKING_REJECTED", async () => { await page.click('[data-testid="rejection-confirm"]'); await page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm")); - // --- check that webhook was called - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); + const [request] = webhookReceiver.requestList; // eslint-disable-next-line @typescript-eslint/no-explicit-any const body = request.body as any; @@ -332,9 +326,8 @@ test.describe("BOOKING_REQUESTED", async () => { // --- check that webhook was called - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); + const [request] = webhookReceiver.requestList; // eslint-disable-next-line @typescript-eslint/no-explicit-any const body = request.body as any; @@ -442,9 +435,7 @@ test.describe("BOOKING_RESCHEDULED", async () => { expect(newBooking).not.toBeNull(); // --- check that webhook was called - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); const [request] = webhookReceiver.requestList; @@ -520,9 +511,7 @@ test.describe("BOOKING_RESCHEDULED", async () => { expect(newBooking).not.toBeNull(); // --- check that webhook was called - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); const [firstRequest] = webhookReceiver.requestList; @@ -541,9 +530,7 @@ test.describe("BOOKING_RESCHEDULED", async () => { await expect(page).toHaveURL(/.*booking/); - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(2); - }); + await webhookReceiver.waitForRequestCount(2); const [_, secondRequest] = webhookReceiver.requestList; @@ -597,9 +584,8 @@ test.describe("FORM_SUBMITTED", async () => { await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe"); page.click('button[type="submit"]'); - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + await webhookReceiver.waitForRequestCount(1); + const [request] = webhookReceiver.requestList; // eslint-disable-next-line @typescript-eslint/no-explicit-any const body = request.body as any; @@ -656,9 +642,9 @@ test.describe("FORM_SUBMITTED", async () => { const fieldName = "name"; await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe"); page.click('button[type="submit"]'); - await waitFor(() => { - expect(webhookReceiver.requestList.length).toBe(1); - }); + + await webhookReceiver.waitForRequestCount(1); + const [request] = webhookReceiver.requestList; // eslint-disable-next-line @typescript-eslint/no-explicit-any const body = request.body as any; From 46fc67f70dd0e98c82fdee5154b91e39eab7be5f Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 23 Oct 2023 01:21:06 +0100 Subject: [PATCH 029/118] fix: Date add 1 day adds 24 hours, not 1 day (#12019) * Date add 1 day adds 24 hours, not 1 day, causing the last date to be lost on dst change * Alternate fix with tests * Extract logic so test file doesnt register tsx --- packages/features/calendars/DatePicker.tsx | 35 +++++------------ .../lib/getAvailableDatesInMonth.test.ts | 39 +++++++++++++++++++ .../calendars/lib/getAvailableDatesInMonth.ts | 32 +++++++++++++++ 3 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 packages/features/calendars/lib/getAvailableDatesInMonth.test.ts create mode 100644 packages/features/calendars/lib/getAvailableDatesInMonth.ts diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index e046aadcea..2bea7d04fa 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -5,6 +5,7 @@ import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { useEmbedStyles } from "@calcom/embed-core/embed-iframe"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import classNames from "@calcom/lib/classNames"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -23,9 +24,9 @@ export type DatePickerProps = { /** which date or dates are currently selected (not tracked from here) */ selected?: Dayjs | Dayjs[] | null; /** defaults to current date. */ - minDate?: Dayjs; + minDate?: Date; /** Furthest date selectable in the future, default = UNLIMITED */ - maxDate?: Dayjs; + maxDate?: Date; /** locale, any IETF language tag, e.g. "hu-HU" - defaults to Browser settings */ locale: string; /** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */ @@ -102,7 +103,7 @@ const NoAvailabilityOverlay = ({ }; const Days = ({ - minDate = dayjs.utc(), + minDate, excludedDates = [], browsingDate, weekStart, @@ -121,30 +122,12 @@ const Days = ({ }) => { // Create placeholder elements for empty days in first week const weekdayOfFirst = browsingDate.date(1).day(); - const currentDate = minDate.utcOffset(browsingDate.utcOffset()); - const availableDates = (includedDates: string[] | undefined) => { - const dates = []; - const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); - for ( - let date = currentDate; - date.isBefore(lastDateOfMonth) || date.isSame(lastDateOfMonth, "day"); - date = date.add(1, "day") - ) { - // even if availableDates is given, filter out the passed included dates - if (includedDates && !includedDates.includes(yyyymmdd(date))) { - continue; - } - dates.push(yyyymmdd(date)); - } - return dates; - }; - const utcBrowsingDateWithOffset = browsingDate.utc().add(browsingDate.utcOffset(), "minute"); - const utcCurrentDateWithOffset = currentDate.utc().add(browsingDate.utcOffset(), "minute"); - - const includedDates = utcCurrentDateWithOffset.isSame(utcBrowsingDateWithOffset, "month") - ? availableDates(props.includedDates) - : props.includedDates; + const includedDates = getAvailableDatesInMonth({ + browsingDate: browsingDate.toDate(), + minDate, + includedDates: props.includedDates, + }); const days: (Dayjs | null)[] = Array((weekdayOfFirst - weekStart + 7) % 7).fill(null); for (let day = 1, dayCount = daysInMonth(browsingDate); day <= dayCount; day++) { diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts new file mode 100644 index 0000000000..10e8fdc147 --- /dev/null +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "vitest"; + +import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; +import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; + +describe("Test Suite: Date Picker", () => { + describe("Calculates the available dates left in the month", () => { + // *) Use right amount of days in given month. (28, 30, 31) + test("it returns the right amount of days in a given month", () => { + const currentDate = new Date(); + const nextMonthDate = new Date(Date.UTC(currentDate.getFullYear(), currentDate.getMonth() + 1)); + + const result = getAvailableDatesInMonth({ + browsingDate: nextMonthDate, + }); + + expect(result).toHaveLength(daysInMonth(nextMonthDate)); + }); + // *) Dates in the past are not available. + test("it doesn't return dates that already passed", () => { + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + }); + + expect(result).toHaveLength(daysInMonth(currentDate) - currentDate.getDate() + 1); + }); + // *) Intersect with included dates. + test("it intersects with given included dates", () => { + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + includedDates: [yyyymmdd(currentDate)], + }); + + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.ts new file mode 100644 index 0000000000..8fbace876b --- /dev/null +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.ts @@ -0,0 +1,32 @@ +import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; + +// calculate the available dates in the month: +// *) Intersect with included dates. +// *) Dates in the past are not available. +// *) Use right amount of days in given month. (28, 30, 31) +export function getAvailableDatesInMonth({ + browsingDate, // pass as UTC + minDate = new Date(), + includedDates, +}: { + browsingDate: Date; + minDate?: Date; + includedDates?: string[]; +}) { + const dates = []; + const lastDateOfMonth = new Date( + Date.UTC(browsingDate.getFullYear(), browsingDate.getMonth(), daysInMonth(browsingDate)) + ); + for ( + let date = browsingDate > minDate ? browsingDate : minDate; + date <= lastDateOfMonth; + date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 1)) + ) { + // intersect included dates + if (includedDates && !includedDates.includes(yyyymmdd(date))) { + continue; + } + dates.push(yyyymmdd(date)); + } + return dates; +} From 6c00c9b2b8bd4b8f8f6ee6b9b605190936849938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vichea=20=E1=9E=9C=E1=9E=B7=E1=9E=87=E1=9F=92=E1=9E=87?= =?UTF-8?q?=E1=9E=B6?= <48352653+vicheanath@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:02:45 -0500 Subject: [PATCH 030/118] =?UTF-8?q?feat:=20km-localization-cambodia=20?= =?UTF-8?q?=F0=9F=87=B0=F0=9F=87=AD=20(#12027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Peer Richelsen Co-authored-by: Peer Richelsen --- apps/web/public/static/locales/km/common.json | 2097 +++++++++++++++++ apps/web/public/static/locales/km/vital.json | 13 + packages/config/next-i18next.config.js | 1 + 3 files changed, 2111 insertions(+) create mode 100644 apps/web/public/static/locales/km/common.json create mode 100644 apps/web/public/static/locales/km/vital.json diff --git a/apps/web/public/static/locales/km/common.json b/apps/web/public/static/locales/km/common.json new file mode 100644 index 0000000000..ed535bcb13 --- /dev/null +++ b/apps/web/public/static/locales/km/common.json @@ -0,0 +1,2097 @@ +{ + "identity_provider": "អ្នកផ្តល់អត្តសញ្ញាណ", + "trial_days_left": "អ្នកនៅសល់ $t(day, {\"count\": {{days}} }) ទៀតក្នុងការសាកល្បង PRO របស់អ្នក។", + "day_one": "{{count}} ថ្ងៃ", + "day_other": "{{count}} ថ្ងៃ", + "second_one": "{{count}} នាទី", + "second_other": "{{count}} នាទី", + "upgrade_now": "ធ្វើបច្ចុប្បន្នភាពឥឡូវនេះ", + "accept_invitation": "ទទួលយកការអញ្ជើញ", + "calcom_explained": "{{appName}} ផ្តល់ហេដ្ឋារចនាសម្ព័ន្ធកំណត់ពេលសម្រាប់មនុស្សគ្រប់គ្នា។", + "calcom_explained_new_user": "បញ្ចប់ការកំណត់របស់អ្នក {{appName}} គណនី! អ្នកនៅសល់តែប៉ុន្មានជំហានទៀតប៉ុណ្ណោះ ក្នុងការដោះស្រាយបញ្ហាការកំណត់កាលវិភាគរបស់អ្នក។", + "have_any_questions": "អ្នកមានសំណួរឬ? យើងនៅទីនេះដើម្បីជួយ។", + "reset_password_subject": "{{appName}}: កំណត់ការណែនាំពាក្យសម្ងាត់ឡើងវិញ", + "verify_email_subject": "{{appName}}: ផ្ទៀងផ្ទាត់គណនីរបស់អ្នក។", + "check_your_email": "ពិនិត្យអ៊ីមែលរបស់អ្នក។", + "verify_email_page_body": "យើងបានផ្ញើអ៊ីមែលទៅ {{email}}។ វាមានសារៈសំខាន់ណាស់ក្នុងការផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នក ដើម្បីធានានូវលទ្ធភាពចែកចាយអ៊ីមែល និងប្រតិទិនដ៏ល្អបំផុតពី {{appName}}.", + "verify_email_banner_body": "ផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នក ដើម្បីធានានូវលទ្ធភាពចែកចាយអ៊ីមែល និងប្រតិទិនដ៏ល្អបំផុត", + "verify_email_email_header": "ផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នក។", + "verify_email_email_button": "ផ្ទៀងផ្ទាត់អ៊ីមែល", + "copy_somewhere_safe": "រក្សាទុក API key នេះនៅកន្លែងណាដែលមានសុវត្ថិភាព។ អ្នកនឹងមិនអាចមើលវាម្តងទៀតបានទេ។", + "verify_email_email_body": "សូមផ្ទៀងផ្ទាត់អ៊ីមែលរបស់អ្នកដោយចុចប៊ូតុងខាងក្រោម។", + "verify_email_by_code_email_body": "សូមផ្ទៀងផ្ទាត់អាសយដ្ឋានអ៊ីមែលរបស់អ្នកដោយប្រើលេខកូដខាងក្រោម។", + "verify_email_email_link_text": "នេះជាតំណភ្ជាប់ក្នុងករណីដែលអ្នកមិនចូលចិត្តចុចប៊ូតុង៖", + "email_verification_code": "សូម​បញ្ចូល​កូដ", + "email_verification_code_placeholder": "បញ្ចូលលេខកូដផ្ទៀងផ្ទាត់ដែលបានផ្ញើទៅអ៊ីមែលរបស់អ្នក។", + "incorrect_email_verification_code": "លេខកូដផ្ទៀងផ្ទាត់មិនត្រឹមត្រូវទេ។", + "email_sent": "អ៊ីមែលត្រូវបានផ្ញើដោយជោគជ័យ", + "email_not_sent": "កំហុសបានកើតឡើងនៅពេលផ្ញើអ៊ីមែល", + "event_declined_subject": "បានបដិសេធ៖ {{title}} នៅ {{date}}", + "event_cancelled_subject": "បានលុបចោល៖ {{title}} នៅ {{date}}", + "event_request_declined": "សំណើព្រឹត្តិការណ៍របស់អ្នកត្រូវបានបដិសេធ", + "event_request_declined_recurring": "សំណើព្រឹត្តិការណ៍កើតឡើងដដែលៗរបស់អ្នកត្រូវបានបដិសេធ", + "event_request_cancelled": "ព្រឹត្តិការណ៍ដែលបានគ្រោងទុករបស់អ្នកត្រូវបានលុបចោល", + "organizer": "អ្នករៀបចំ", + "need_to_reschedule_or_cancel": "ត្រូវ​ការ​កំណត់​ពេល​វេលា​ឡើងវិញ​ឬ​បោះបង់?", + "no_options_available": "មិនមានជម្រើសទេ។", + "cancellation_reason": "ហេតុផលសម្រាប់ការលុបចោល (មិនចាំបាច់)", + "cancellation_reason_placeholder": "ហេតុអ្វីបានជាអ្នកលុបចោល?", + "rejection_reason": "ហេតុផលសម្រាប់ការបដិសេធ", + "rejection_reason_title": "បដិសេធសំណើកក់?", + "rejection_reason_description": "តើអ្នកប្រាកដថាចង់បដិសេធការកក់នេះទេ? យើង​នឹង​ឲ្យ​អ្នក​ដែល​ព្យាយាម​កក់​នោះ​ដឹង។ អ្នកអាចផ្តល់ហេតុផលខាងក្រោម។", + "rejection_confirmation": "បដិសេធការកក់", + "manage_this_event": "គ្រប់គ្រងព្រឹត្តិការណ៍នេះ។", + "invite_team_member": "អញ្ជើញសមាជិកក្រុម", + "invite_team_individual_segment": "អញ្ជើញជាបុគ្គល", + "invite_team_bulk_segment": "ការនាំចូលច្រើន", + "invite_team_notifcation_badge": "Inv.", + "your_event_has_been_scheduled": "ព្រឹត្តិការណ៍របស់អ្នកត្រូវបានកំណត់ពេល", + "your_event_has_been_scheduled_recurring": "ព្រឹត្តិការណ៍កើតឡើងដដែលៗរបស់អ្នកត្រូវបានកំណត់ពេល", + "accept_our_license": "ទទួលយកអាជ្ញាប័ណ្ណរបស់យើងដោយការផ្លាស់ប្តូរ .env អថេរ <1>NEXT_PUBLIC_LICENSE_CONSENT ទៅ '{{agree}}'.", + "remove_banner_instructions": "ដើម្បីលុបបដានេះ សូមបើកឯកសារ .env របស់អ្នក ហើយផ្លាស់ប្តូរ <1>NEXT_PUBLIC_LICENSE_CONSENT អថេរទៅ '{{agree}}'.", + "error_message": "សារកំហុសគឺ៖ '{{errorMessage}}'", + "refund_failed_subject": "ការសងប្រាក់វិញបានបរាជ័យ៖ {{name}} - {{date}} - {{eventType}}", + "refund_failed": "ការសងប្រាក់វិញសម្រាប់ព្រឹត្តិការណ៍ {{eventType}} សម្រាប់ {{userName}} នៅថ្ងៃ {{date}} បរាជ័យ។", + "check_with_provider_and_user": "សូមពិនិត្យជាមួយអ្នកផ្តល់សេវាទូទាត់របស់អ្នក និង {{user}} របៀបដោះស្រាយនេះ។", + "a_refund_failed": "ការសងប្រាក់វិញបានបរាជ័យ", + "awaiting_payment_subject": "កំពុងរង់ចាំការទូទាត់៖ {{title}} នៅថ្ងៃ {{date}}", + "meeting_awaiting_payment": "ការប្រជុំរបស់អ្នកកំពុងរង់ចាំការបង់ប្រាក់", + "help": "ជំនួយ", + "price": "តម្លៃ", + "paid": "បង់ប្រាក់", + "refunded": "សងប្រាក់វិញ។", + "payment": "ការទូទាត់", + "missing_card_fields": "ប្រអប់ កាត ត្រូវតែបំពេញ", + "pay_now": "បង់ប្រាក់ឥឡូវនេះ", + "codebase_has_to_stay_opensource": "មូលដ្ឋានកូដត្រូវតែរក្សាប្រភពបើកចំហ ទោះបីជាវាត្រូវបានកែប្រែឬអត់ក៏ដោយ។", + "cannot_repackage_codebase": "អ្នកមិនអាចវេចខ្ចប់ឡើងវិញ ឬលក់មូលដ្ឋានកូដបានទេ។", + "acquire_license": "ទទួលបានអាជ្ញាប័ណ្ណពាណិជ្ជកម្ម ដើម្បីលុបលក្ខខណ្ឌទាំងនេះដោយការផ្ញើអ៊ីមែល", + "terms_summary": "សេចក្តីសង្ខេបនៃលក្ខខណ្ឌ", + "open_env": "បើកឯកសារ .env ហើយយល់ព្រមនឹងអាជ្ញាប័ណ្ណរបស់យើង។", + "env_changed": "ខ្ញុំបានផ្លាស់ប្តូរឯកសារ .env របស់ខ្ញុំ", + "accept_license": "ទទួលយកអាជ្ញាប័ណ្ណ", + "still_waiting_for_approval": "ព្រឹត្តិការណ៍មួយនៅតែរង់ចាំការយល់ព្រម", + "event_is_still_waiting": "សំណើព្រឹត្តិការណ៍កំពុងរង់ចាំ៖ {{attendeeName}} - {{date}} - {{eventType}}", + "no_more_results": "មិនមានលទ្ធផលទៀតទេ", + "no_results": "គ្មាន​លទ្ធផល", + "load_more_results": "ផ្ទុកលទ្ធផលបន្ថែមទៀត", + "integration_meeting_id": "{{integrationName}} លេខសម្គាល់ការប្រជុំ៖ {{meetingId}}", + "confirmed_event_type_subject": "បញ្ជាក់៖ {{eventType}} ជាមួយ {{name}} នៅ {{date}}", + "new_event_request": "សំណើព្រឹត្តិការណ៍ថ្មី៖ {{attendeeName}} - {{date}} - {{eventType}}", + "confirm_or_reject_request": "បញ្ជាក់ ឬបដិសេធសំណើ", + "check_bookings_page_to_confirm_or_reject": "ពិនិត្យមើលទំព័រកក់របស់អ្នក ដើម្បីបញ្ជាក់ ឬបដិសេធការកក់។", + "event_awaiting_approval": "ព្រឹត្តិការណ៍មួយកំពុងរង់ចាំការយល់ព្រមរបស់អ្នក។", + "event_awaiting_approval_recurring": "ព្រឹត្តិការណ៍កើតឡើងដដែលៗកំពុងរង់ចាំការយល់ព្រមរបស់អ្នក។", + "someone_requested_an_event": "មាននរណាម្នាក់បានស្នើសុំរៀបចំព្រឹត្តិការណ៍មួយនៅលើប្រតិទិនរបស់អ្នក។", + "someone_requested_password_reset": "មាននរណាម្នាក់បានស្នើសុំតំណដើម្បីផ្លាស់ប្តូរពាក្យសម្ងាត់របស់អ្នក។", + "password_reset_email_sent": "ប្រសិនបើអ៊ីមែលនេះមាននៅក្នុងប្រព័ន្ធរបស់យើង អ្នកគួរតែទទួលបានអ៊ីមែលកំណត់ឡើងវិញ។", + "password_reset_instructions": "ប្រសិនបើអ្នកមិនបានស្នើសុំវាទេ អ្នកអាចមិនអើពើអ៊ីមែលនេះដោយសុវត្ថិភាព ហើយពាក្យសម្ងាត់របស់អ្នកនឹងមិនត្រូវបានផ្លាស់ប្តូរទេ។", + "event_awaiting_approval_subject": "កំពុងរង់ចាំការអនុម័ត៖ {{title}} នៅថ្ងៃ {{date}}", + "event_still_awaiting_approval": "ព្រឹត្តិការណ៍មួយកំពុងរង់ចាំការយល់ព្រមរបស់អ្នក។", + "booking_submitted_subject": "បានដាក់ស្នើការកក់ទុក៖ {{title}} នៅថ្ងៃ {{date}}", + "download_recording_subject": "ទាញយកការថត៖ {{title}} នៅថ្ងៃ {{date}}", + "download_your_recording": "ទាញយកការថតរបស់អ្នក។", + "your_meeting_has_been_booked": "ការប្រជុំរបស់អ្នកត្រូវបានកក់ទុក", + "event_type_has_been_rescheduled_on_time_date": "{{title}} របស់អ្នកត្រូវបានកំណត់ពេលវេលាឡើងវិញ ទៅថ្ងៃ {{date}}.", + "event_has_been_rescheduled": "បានធ្វើបច្ចុប្បន្នភាព - ព្រឹត្តិការណ៍របស់អ្នកត្រូវបានកំណត់ពេលឡើងវិញ", + "request_reschedule_subtitle": "{{organizer}} បានលុបចោលការកក់ ហើយស្នើឱ្យអ្នកជ្រើសរើសពេលផ្សេងទៀត។", + "request_reschedule_title_organizer": "អ្នកបានស្នើសុំ {{attendee}} ដើម្បីរៀបចំកាលវិភាគឡើងវិញ", + "request_reschedule_subtitle_organizer": "អ្នកបានលុបចោលការកក់ហើយ {{attendee}} គួរតែជ្រើសរើសពេលវេលាកក់ថ្មីជាមួយអ្នក។", + "rescheduled_event_type_subject": "សំណើសម្រាប់កាលវិភាគត្រូវបានផ្ញើឡើងវិញ៖ {{eventType}} ជាមួយ {{name}} នៅថ្ងៃ {{date}}", + "requested_to_reschedule_subject_attendee": "ត្រូវតែកាលវិភាគឡើងវិញ: សូមកក់ពេលវេលាថ្មីសម្រាប់ {{eventType}} ជាមួយ {{name}}", + "hi_user_name": "សួស្តី {{name}}", + "ics_event_title": "{{eventType}} ជាមួយ {{name}}", + "new_event_subject": "ព្រឹត្តិការណ៍ថ្មី៖ {{attendeeName}} - {{date}} - {{eventType}}", + "join_by_entrypoint": "ចូលរួមដោយ {{entryPoint}}", + "notes": "កំណត់ចំណាំ", + "manage_my_bookings": "គ្រប់គ្រងការកក់របស់ខ្ញុំ", + "need_to_make_a_change": "ត្រូវ​ការ​ផ្លាស់​ប្តូ​រ​?", + "new_event_scheduled": "ព្រឹត្តិការណ៍ថ្មីមួយត្រូវបានកំណត់ពេល។", + "new_event_scheduled_recurring": "ព្រឹត្តិការណ៍កើតឡើងម្តងទៀតត្រូវបានកំណត់ពេល។", + "invitee_email": "អ៊ីមែលអញ្ជើញ", + "invitee_timezone": "តំបន់ពេលវេលាអញ្ជើញ", + "time_left": "ពេលវេលានៅសល់", + "event_type": "ប្រភេទព្រឹត្តិការណ៍", + "enter_meeting": "ចូលប្រជុំ", + "video_call_provider": "អ្នកផ្តល់សេវាហៅជាវីដេអូ", + "meeting_id": "លេខសម្គាល់ការប្រជុំ", + "meeting_password": "ពាក្យសម្ងាត់ការប្រជុំ", + "meeting_url": "URL ការប្រជុំ", + "meeting_request_rejected": "សំណើប្រជុំរបស់អ្នកត្រូវបានបដិសេធ", + "rejected_event_type_with_organizer": "ច្រានចោល៖ {{eventType}} ជាមួយ {{organizer}} នៅថ្ងៃ {{date}}", + "hi": "សួស្តី", + "join_team": "ចូលរួមក្រុម", + "manage_this_team": "គ្រប់គ្រងក្រុមនេះ", + "team_info": "ព័ត៌មានក្រុម", + "request_another_invitation_email": "ប្រសិនបើអ្នកមិនចង់ប្រើ {{toEmail}} ជា {{appName}} របស់អ្នក អ៊ីម៉ែលឬ គណនី {{appName}} មានរួចហើយ, សូមស្នើសុំការអញ្ជើញមួយផ្សេងទៀតទៅកាន់អ៊ីមែលនោះ។", + "you_have_been_invited": "អ្នកត្រូវបានអញ្ជើញឱ្យចូលរួមក្រុម {{teamName}}", + "user_invited_you": "{{user}} បានអញ្ជើញអ្នកឱ្យចូលរួម {{entity}} {{team}} នៅលើ {{appName}}", + "hidden_team_member_title": "អ្នកត្រូវបានលាក់នៅក្នុងក្រុមនេះ។", + "hidden_team_member_message": "កៅអីរបស់អ្នកមិនត្រូវបានបង់ទេ ទាំង Upgrade ទៅ PRO ឬអនុញ្ញាតឱ្យម្ចាស់ក្រុមដឹងថាពួកគេអាចបង់ប្រាក់សម្រាប់កៅអីរបស់អ្នក។", + "hidden_team_owner_message": "អ្នក​ត្រូវ​ការ​គណនី​គាំទ្រ​ដើម្បី​ប្រើ​ក្រុម អ្នក​ត្រូវ​បាន​លាក់​រហូត​ដល់​អ្នក Upgrade", + "link_expires": "p.s. វាផុតកំណត់នៅក្នុង {{expiresIn}} ម៉ោង", + "upgrade_to_per_seat": "Upgrade ទៅ Per-Seat", + "seat_options_doesnt_support_confirmation": "ជម្រើសកៅអីមិនគាំទ្រតម្រូវការបញ្ជាក់ទេ។", + "team_upgrade_seats_details": "ក្នុងចំណោមសមាជិក {{memberCount}} នាក់នៅក្នុងក្រុមរបស់អ្នក។, {{unpaidCount}} កន្លែងអង្គុយមិនបង់ប្រាក់ទេ។ នៅ ${{seatPrice}}/ខែក្នុងមួយកៅអី តម្លៃសរុបប៉ាន់ស្មាននៃសមាជិកភាពរបស់អ្នកគឺ ${{totalCost}}/ខែ.", + "team_upgrade_banner_description": "អ្នកមិនទាន់បានបញ្ចប់ការរៀបចំក្រុមរបស់អ្នកទេ។ ក្រុមរបស់អ្នក \"{{teamName}}\" ត្រូវការធ្វើឱ្យប្រសើរឡើង។", + "upgrade_banner_action": "Upgrade ទីនេះ", + "team_upgraded_successfully": "ក្រុមរបស់អ្នកត្រូវបាន upgrade ដោយជោគជ័យ!", + "org_upgrade_banner_description": "សូមអរគុណសម្រាប់ការសាកល្បងគម្រោង Organization របស់យើង។ យើងកត់សំគាល់ Organization របស់អ្នក \"{{teamName}}\" ត្រូវការធ្វើឱ្យប្រសើរឡើង។", + "org_upgraded_successfully": "Organization របស់អ្នកត្រូវបានដំឡើងកំណែដោយជោគជ័យ!", + "use_link_to_reset_password": "ប្រើតំណខាងក្រោមដើម្បីកំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញ", + "hey_there": "ហេ!", + "forgot_your_password_calcom": "ភ្លេចពាក្យសម្ងាត់? - {{appName}}", + "delete_webhook_confirmation_message": "តើអ្នកប្រាកដថាចង់លុប webhook នេះទេ? អ្នកនឹងលែងទទួលបានទិន្នន័យប្រជុំ {{appName}} តាម URL ដែលបានបញ្ជាក់, ក្នុងពេលវេលាជាក់ស្តែង នៅពេលដែលព្រឹត្តិការណ៍មួយត្រូវបានកំណត់ពេល ឬលុបចោល។", + "confirm_delete_webhook": "បាទ/ចាស លុប webhook", + "edit_webhook": "កែសម្រួល Webhook", + "delete_webhook": "លុប Webhook", + "webhook_status": "ស្ថានភាព Webhook", + "webhook_enabled": "Webhook បានបើក", + "webhook_disabled": "Webhook បានបិទ", + "webhook_response": "ការឆ្លើយតប Webhook", + "webhook_test": "តេស្ត Webhook", + "manage_your_webhook": "គ្រប់គ្រង webhook របស់អ្នក។", + "webhook_created_successfully": "Webhook បានបង្កើតដោយជោគជ័យ!", + "webhook_updated_successfully": "Webhook បានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ!", + "webhook_removed_successfully": "Webhook ត្រូវបានដកចេញដោយជោគជ័យ!", + "payload_template": "គំរូនៃការផ្ទុក", + "dismiss": "ច្រានចោល", + "no_data_yet": "មិនមានទិន្នន័យនៅឡើយទេ", + "ping_test": "ការធ្វើតេស្តភីង(Ping)", + "add_to_homescreen": "បន្ថែមកម្មវិធីនេះទៅអេក្រង់ដើមរបស់អ្នកសម្រាប់ការចូលប្រើកាន់តែលឿន និងបទពិសោធន៍ប្រសើរឡើង។", + "upcoming": "នាពេលខាងមុខ", + "recurring": "កើតឡើងម្តងទៀត", + "past": "អតីតកាល", + "choose_a_file": "ជ្រើសរើសឯកសារ...", + "upload_image": "បង្ហោះរូបភាព", + "upload_target": "បង្ហោះ {{target}}", + "no_target": "គ្មាន {{target}}", + "slide_zoom_drag_instructions": "អូសដើម្បីពង្រីក អូសដើម្បីដាក់ទីតាំងឡើងវិញ", + "view_notifications": "មើលការជូនដំណឹង", + "view_public_page": "មើលទំព័រសាធារណៈ", + "copy_public_page_link": "ចម្លងតំណទំព័រសាធារណៈ", + "sign_out": "ចាកចេញ", + "add_another": "បន្ថែមមួយទៀត", + "install_another": "ដំឡើងមួយផ្សេងទៀត", + "until": "រហូតដល់", + "powered_by": "ដំណើរការដោយ", + "unavailable": "មិន​មាន", + "set_work_schedule": "កំណត់កាលវិភាគការងាររបស់អ្នក។", + "change_bookings_availability": "ផ្លាស់ប្តូរនៅពេលដែលអ្នកមានសម្រាប់ការកក់", + "select": "ជ្រើសរើស...", + "2fa_confirm_current_password": "បញ្ជាក់ពាក្យសម្ងាត់បច្ចុប្បន្នរបស់អ្នក ដើម្បីចាប់ផ្តើម។", + "2fa_scan_image_or_use_code": "ស្កេនរូបភាពខាងក្រោមដោយប្រើកម្មវិធីផ្ទៀងផ្ទាត់(Authenticator App) ឬបញ្ចូលលេខកូដអត្ថបទដោយដៃជំនួសវិញ។", + "text": "អត្ថបទ", + "multiline_text": "អត្ថបទច្រើនបន្ទាត់", + "number": "ចំនួន", + "checkbox": "ប្រអប់ធីក", + "is_required": "គឺ​តំរូវ​អោយ​មាន", + "required": "ទាមទារ", + "optional": "ស្រេចចិត្ត", + "input_type": "ប្រភេទបញ្ចូល", + "rejected": "បដិសេធ", + "unconfirmed": "មិន​បាន​បញ្ជាក់", + "guests": "ភ្ញៀវ", + "guest": "ភ្ញៀវ", + "web_conferencing_details_to_follow": "Web conferencing details to follow in the confirmation email.", + "404_the_user": "The username", + "username": "Username", + "is_still_available": "is still available.", + "documentation": "Documentation", + "documentation_description": "Learn how to integrate our tools with your app", + "api_reference": "API Reference", + "api_reference_description": "A complete API reference for our libraries", + "blog": "Blog", + "blog_description": "Read our latest news and articles", + "join_our_community": "Join our community", + "join_our_discord": "Join our Discord", + "404_claim_entity_user": "Claim your username and schedule events", + "popular_pages": "Popular pages", + "register_now": "Register now", + "register": "Register", + "page_doesnt_exist": "This page does not exist.", + "check_spelling_mistakes_or_go_back": "Check for spelling mistakes or go back to the previous page.", + "404_page_not_found": "404: This page could not be found.", + "booker_event_not_found": "We could not find the event you are trying to book.", + "getting_started": "Getting Started", + "15min_meeting": "15 Min Meeting", + "30min_meeting": "30 Min Meeting", + "secret": "Secret", + "leave_blank_to_remove_secret": "Leave blank to remove secret", + "webhook_secret_key_description": "Ensure your server is only receiving the expected {{appName}} requests for security reasons", + "secret_meeting": "Secret Meeting", + "login_instead": "Login instead", + "already_have_an_account": "Already have an account?", + "create_account": "Create Account", + "confirm_password": "Confirm password", + "confirm_auth_change": "This will change the way you log in", + "confirm_auth_email_change": "Changing the email address will disconnect your current authentication method to log in to Cal.com. We will ask you to verify your new email address. Moving forward, you will be logged out and use your new email address to log in instead of your current authentication method after setting your password by following the instructions that will be sent to your mail.", + "reset_your_password": "Set your new password with the instructions sent to your email address.", + "email_change": "Log back in with your new email address and password.", + "create_your_account": "Create your account", + "sign_up": "Sign up", + "youve_been_logged_out": "You've been logged out", + "hope_to_see_you_soon": "We hope to see you again soon!", + "logged_out": "Logged out", + "please_try_again_and_contact_us": "Please try again and contact us if the issue persists.", + "incorrect_2fa_code": "Two-factor code is incorrect.", + "no_account_exists": "No account exists matching that email address.", + "2fa_enabled_instructions": "Two-factor authentication enabled. Please enter the six-digit code from your authenticator app.", + "2fa_enter_six_digit_code": "Enter the six-digit code from your authenticator app below.", + "create_an_account": "Create an account", + "dont_have_an_account": "Don't have an account?", + "2fa_code": "Two-Factor Code", + "sign_in_account": "Sign in to your account", + "sign_in": "Sign in", + "go_back_login": "Go back to the login page", + "error_during_login": "An error occurred when logging you in. Head back to the login screen and try again.", + "request_password_reset": "Send reset email", + "send_invite": "Send invite", + "forgot_password": "Forgot Password?", + "forgot": "Forgot?", + "done": "Done", + "all_done": "All done!", + "all": "All", + "yours": "Your account", + "available_apps": "Available Apps", + "available_apps_lower_case": "Available apps", + "available_apps_desc": "View popular apps below and explore more in our <1>App Store", + "fixed_host_helper": "Add anyone who needs to attend the event. <1>Learn more", + "round_robin_helper":"People in the group take turns and only one person will show up for the event.", + "check_email_reset_password": "Check your email. We sent you a link to reset your password.", + "finish": "Finish", + "organization_general_description": "Manage settings for your team language and timezone", + "few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.", + "nearly_there": "Nearly there!", + "nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.", + "set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.", + "set_availability": "Set your availability", + "availability_settings": "Availability Settings", + "continue_without_calendar": "Continue without calendar", + "continue_with": "Continue with {{appName}}", + "connect_your_calendar": "Connect your calendar", + "connect_your_video_app": "Connect your video apps", + "connect_your_video_app_instructions": "Connect your video apps to use them on your event types.", + "connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.", + "set_up_later": "Set up later", + "current_time": "Current time", + "details": "Details", + "welcome": "Welcome", + "welcome_back": "Welcome back", + "welcome_to_calcom": "Welcome to {{appName}}", + "welcome_instructions": "Tell us what to call you and let us know what timezone you’re in. You’ll be able to edit this later.", + "connect_caldav": "Connect to CalDav (Beta)", + "connect": "Connect", + "try_for_free": "Try it for free", + "create_booking_link_with_calcom": "Create your own booking link with {{appName}}", + "who": "Who", + "what": "What", + "when": "When", + "where": "Where", + "add_to_calendar": "Add to calendar", + "add_to_calendar_description":"Select where to add events when you’re booked.", + "add_events_to":"Add events to", + "add_another_calendar": "Add another calendar", + "other": "Other", + "email_sign_in_subject": "Your sign-in link for {{appName}}", + "emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.", + "emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.", + "emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.", + "needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.", + "needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.", + "user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.", + "user_needs_to_confirm_or_reject_booking_recurring": "{{user}} still needs to confirm or reject each booking of the recurring meeting.", + "meeting_is_scheduled": "This meeting is scheduled", + "meeting_is_scheduled_recurring": "The recurring events are scheduled", + "booking_submitted": "Your booking has been submitted", + "booking_submitted_recurring": "Your recurring meeting has been submitted", + "booking_confirmed": "Your booking has been confirmed", + "booking_confirmed_recurring": "Your recurring meeting has been confirmed", + "warning_recurring_event_payment": "Payments are not supported with Recurring Events yet", + "warning_payment_recurring_event": "Recurring events are not supported with Payments yet", + "enter_new_password": "Enter the new password you'd like for your account.", + "reset_password": "Reset Password", + "change_your_password": "Change your password", + "show_password": "Show password", + "hide_password": "Hide password", + "try_again": "Try Again", + "request_is_expired": "That Request is Expired.", + "reset_instructions": "Enter the email address associated with your account and we will send you a link to reset your password.", + "request_is_expired_instructions": "That request is expired. Go back and enter the email associated with your account and we will send you another link to reset your password.", + "whoops": "Whoops", + "login": "Login", + "success": "Success", + "failed": "Failed", + "password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.", + "layout": "Layout", + "bookerlayout_default_title": "Default view", + "bookerlayout_description": "You can select multiple and your bookers can switch views.", + "bookerlayout_user_settings_title": "Booking layout", + "bookerlayout_user_settings_description": "You can select multiple and bookers can switch views. This can be overridden on a per event basis.", + "bookerlayout_month_view": "Month", + "bookerlayout_week_view": "Weekly", + "bookerlayout_column_view": "Column", + "bookerlayout_error_min_one_enabled": "At least one layout has to be enabled.", + "bookerlayout_error_default_not_enabled": "The layout you selected as the default view is not part of the enabled layouts.", + "bookerlayout_error_unknown_layout": "The layout you selected is not a valid layout.", + "bookerlayout_override_global_settings": "You can manage this for all your event types in Settings -> <2>Appearance or <6>Override for this event only.", + "unexpected_error_try_again": "An unexpected error occurred. Try again.", + "sunday_time_error": "Invalid time on Sunday", + "monday_time_error": "Invalid time on Monday", + "tuesday_time_error": "Invalid time on Tuesday", + "wednesday_time_error": "Invalid time on Wednesday", + "thursday_time_error": "Invalid time on Thursday", + "friday_time_error": "Invalid time on Friday", + "saturday_time_error": "Invalid time on Saturday", + "error_end_time_before_start_time": "End time cannot be before start time", + "error_end_time_next_day": "End time cannot be greater than 24 hours", + "back_to_bookings": "Back to bookings", + "free_to_pick_another_event_type": "Feel free to pick another event anytime.", + "cancelled": "Canceled", + "cancellation_successful": "Cancellation successful", + "really_cancel_booking": "Really cancel your booking?", + "cannot_cancel_booking": "You cannot cancel this booking", + "reschedule_instead": "Instead, you could also reschedule it.", + "event_is_in_the_past": "The event is in the past", + "cancelling_event_recurring": "The event is one instance of a recurring event.", + "cancelling_all_recurring": "These are all remaining instances in the recurring event.", + "error_with_status_code_occured": "An error with status code {{status}} occurred.", + "error_event_type_url_duplicate": "An event type with this URL already exists.", + "error_event_type_unauthorized_create": "You are not able to create this event", + "error_event_type_unauthorized_update": "You are not able to edit this event", + "error_workflow_unauthorized_create": "You are not able to create this workflow", + "error_schedule_unauthorized_create": "You are not able to create this schedule", + "booking_already_cancelled": "This booking was already canceled", + "booking_already_accepted_rejected": "This booking was already accepted or rejected", + "go_back_home": "Go back home", + "or_go_back_home": "Or go back home", + "no_meeting_found": "No Meeting Found", + "no_meeting_found_description": "This meeting does not exist. Contact the meeting owner for an updated link.", + "no_status_bookings_yet": "No {{status}} bookings", + "no_status_bookings_yet_description": "You have no {{status}} bookings. {{description}}", + "event_between_users": "{{eventName}} between {{host}} and {{attendeeName}}", + "bookings": "Bookings", + "booking_not_found": "Booking not found", + "bookings_description": "See upcoming and past events booked through your event type links.", + "upcoming_bookings": "As soon as someone books a time with you it will show up here.", + "recurring_bookings": "As soon as someone books a recurring meeting with you it will show up here.", + "past_bookings": "Your past bookings will show up here.", + "cancelled_bookings": "Your canceled bookings will show up here.", + "unconfirmed_bookings": "Your unconfirmed bookings will show up here.", + "unconfirmed_bookings_tooltip": "Unconfirmed bookings", + "on": "on", + "and": "and", + "calendar_shows_busy_between": "Your calendar shows you as busy between", + "troubleshoot": "Troubleshoot", + "troubleshoot_description": "Understand why certain times are available and others are blocked.", + "overview_of_day": "Here is an overview of your day on", + "hover_over_bold_times_tip": "Tip: Hover over the bold times for a full timestamp", + "start_time": "Start time", + "end_time": "End time", + "buffer_time": "Buffer time", + "before_event": "Before event", + "after_event": "After event", + "event_buffer_default": "No buffer time", + "buffer": "Buffer", + "your_day_starts_at": "Your day starts at", + "your_day_ends_at": "Your day ends at", + "launch_troubleshooter": "Launch troubleshooter", + "troubleshoot_availability": "Troubleshoot your availability to explore why your times are showing as they are.", + "change_available_times": "Change available times", + "change_your_available_times": "Change your available times", + "change_start_end": "Change the start and end times of your day", + "change_start_end_buffer": "Set the start and end time of your day and a minimum buffer between your meetings.", + "current_start_date": "Currently, your day is set to start at", + "start_end_changed_successfully": "The start and end times for your day have been changed successfully.", + "and_end_at": "and end at", + "light": "Light", + "dark": "Dark", + "automatically_adjust_theme": "Automatically adjust theme based on invitee preferences", + "user_dynamic_booking_disabled": "Some of the users in the group have currently disabled dynamic group bookings", + "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_placeholder": "jdoe@example.com", + "full_name": "ឈ្មោះ​ពេញ", + "browse_api_documentation": "Browse our API documentation", + "leverage_our_api": "Leverage our API for full control and customizability.", + "create_webhook": "Create Webhook", + "booking_cancelled": "Booking Canceled", + "booking_rescheduled": "Booking Rescheduled", + "recording_ready": "Recording Download Link Ready", + "booking_created": "Booking Created", + "booking_rejected": "Booking Rejected", + "booking_requested": "Booking Requested", + "booking_payment_initiated": "Booking Payment Initiated", + "meeting_ended": "Meeting Ended", + "form_submitted": "Form Submitted", + "booking_paid": "Booking Paid", + "event_triggers": "Event Triggers", + "subscriber_url": "Subscriber URL", + "create_new_webhook": "Create a new webhook", + "webhooks": "Webhooks", + "team_webhooks": "Team Webhooks", + "create_new_webhook_to_account": "Create a new webhook to your account", + "new_webhook": "New Webhook", + "receive_cal_meeting_data": "Receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.", + "receive_cal_event_meeting_data": "Receive {{appName}} meeting data at a specified URL, in real-time, when this event is scheduled or canceled.", + "responsive_fullscreen_iframe": "Responsive full screen iframe", + "loading": "Loading...", + "deleting": "Deleting...", + "standard_iframe": "Standard iframe", + "developer": "Developer", + "manage_developer_settings": "Manage your developer settings.", + "iframe_embed": "iframe Embed", + "embed_calcom": "The easiest way to embed {{appName}} on your website.", + "integrate_using_embed_or_webhooks": "Integrate with your website using our embed options, or get real-time booking information using custom webhooks.", + "schedule_a_meeting": "Schedule a meeting", + "view_and_manage_billing_details": "View and manage your billing details", + "view_and_edit_billing_details": "View and edit your billing details, as well as cancel your subscription.", + "go_to_billing_portal": "Go to the billing portal", + "need_anything_else": "Need anything else?", + "further_billing_help": "If you need any further help with billing, our support team are here to help.", + "contact": "Contact", + "our_support_team": "our support team", + "contact_our_support_team": "Contact our support team", + "uh_oh": "Uh oh!", + "no_event_types_have_been_setup": "This user hasn't set up any event types yet.", + "edit_logo": "Edit logo", + "upload_a_logo": "Upload a logo", + "upload_logo": "Upload logo", + "remove_logo": "Remove logo", + "enable": "Enable", + "code": "Code", + "code_is_incorrect": "Code is incorrect.", + "add_time_availability": "Add new time slot", + "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", + "2fa": "Two-Factor Authentication", + "2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication", + "enable_2fa": "Enable two-factor authentication", + "disable_2fa": "Disable two-factor authentication", + "disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", + "error_disabling_2fa": "Error disabling two-factor authentication", + "error_enabling_2fa": "Error setting up two-factor authentication", + "security": "Security", + "manage_account_security": "Manage your account's security.", + "password": "Password", + "password_updated_successfully": "Password updated successfully", + "password_has_been_changed": "Your password has been successfully changed.", + "error_changing_password": "Error changing password", + "session_timeout_changed": "Your session configuration has been updated successfully.", + "session_timeout_change_error": "Error updating session configuration", + "something_went_wrong": "Something went wrong.", + "something_doesnt_look_right": "Something doesn't look right?", + "please_try_again": "Please try again.", + "super_secure_new_password": "Your super secure new password", + "new_password": "New Password", + "your_old_password": "Your old password", + "current_password": "Current Password", + "change_password": "Change Password", + "change_secret": "Change Secret", + "new_password_matches_old_password": "New password matches your old password. Please choose a different password.", + "forgotten_secret_description": "If you have lost or forgotten this secret, you can change it, but be aware that all integrations using this secret will need to be updated", + "current_incorrect_password": "Current password is incorrect", + "password_hint_caplow": "Mix of uppercase & lowercase letters", + "password_hint_min": "Minimum 7 characters long", + "password_hint_admin_min": "Minimum 15 characters long", + "password_hint_num": "Contain at least 1 number", + "max_limit_allowed_hint": "Must be {{limit}} or fewer characters long", + "invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters", + "incorrect_password": "Password is incorrect.", + "incorrect_email_password": "Email or password is incorrect.", + "use_setting": "Use setting", + "am_pm": "am/pm", + "time_options": "Time options", + "january": "មករា", + "february": "កុម្ភៈ", + "march": "មីនា", + "april": "មេសា", + "may": "ឧសភា", + "june": "មិថុនា", + "july": "កក្កដា", + "august": "សីហា", + "september": "កញ្ញា", + "october": "តុលា", + "november": "វិច្ឆិកា", + "december": "ធ្នូ", + "monday": "ច័ន្ទ", + "tuesday": "អង្គារ", + "wednesday": "ពុធ", + "thursday": "ព្រហស្បតិ៍", + "friday": "សុក្រ", + "saturday": "សៅរ៍", + "sunday": "អាទិត្យ", + "all_booked_today": "All booked.", + "slots_load_fail": "Could not load the available time slots.", + "additional_guests": "Add guests", + "your_name": "Your name", + "your_full_name": "Your full name", + "no_name": "No name", + "enter_number_between_range": "Please enter a number between 1 and {{maxOccurences}}", + "email_address": "Email address", + "enter_valid_email": "Please enter a valid email", + "location": "Location", + "address": "Address", + "enter_address": "Enter address", + "in_person_attendee_address": "In Person (Attendee Address)", + "yes": "yes", + "no": "no", + "additional_notes": "Additional notes", + "booking_fail": "Could not book the meeting.", + "reschedule_fail": "Could not reschedule the meeting.", + "share_additional_notes": "Please share anything that will help prepare for our meeting.", + "booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}", + "booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}", + "in_person_meeting": "In-person meeting", + "in_person": "In Person (Organizer Address)", + "link_meeting": "Link meeting", + "phone_number": "Phone Number", + "attendee_phone_number": "Attendee Phone Number", + "organizer_phone_number": "Organizer Phone Number", + "enter_phone_number": "Enter phone number", + "reschedule": "Reschedule", + "reschedule_this": "Reschedule instead", + "book_a_team_member": "Book a team member instead", + "or": "OR", + "go_back": "Go back", + "email_or_username": "Email or Username", + "send_invite_email": "Send an invite email", + "role": "Role", + "edit_role": "Edit Role", + "edit_team": "Edit team", + "reject": "Reject", + "reject_all": "Reject all", + "accept": "Accept", + "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", + "your_team_updated_successfully": "Your team has been updated successfully.", + "your_org_updated_successfully": "Your Org has been updated successfully.", + "about": "About", + "team_description": "A few sentences about your team. This will appear on your team's url page.", + "org_description": "A few sentences about your organization. This will appear on your organization's url page.", + "members": "Members", + "organization_members": "Organization members", + "member": "Member", + "number_member_one": "{{count}} member", + "number_member_other": "{{count}} members", + "number_selected": "{{count}} selected", + "owner": "Owner", + "admin": "Admin", + "administrator_user": "Administrator user", + "lets_create_first_administrator_user": "Let's create the first administrator user.", + "admin_user_created": "Administrator user setup", + "admin_user_created_description": "You have already created an administrator user. You can now log in to your account.", + "new_member": "New Member", + "invite": "Invite", + "add_team_members": "Add team members", + "add_team_members_description": "Invite others to join your team", + "add_team_member": "Add team member", + "invite_new_member": "Invite a new team member", + "invite_new_member_description": "Note: This will <1>cost an extra seat ($15/m) on your subscription.", + "invite_new_team_member": "Invite someone to your team.", + "upload_csv_file": "Upload a .csv file", + "invite_via_email": "Invite via email", + "change_member_role": "Change team member role", + "disable_cal_branding": "Disable {{appName}} branding", + "disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.", + "hide_book_a_team_member": "Hide Book a Team Member Button", + "hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.", + "danger_zone": "Danger zone", + "account_deletion_cannot_be_undone":"Careful. Account deletion cannot be undone.", + "back": "Back", + "cancel": "Cancel", + "cancel_all_remaining": "Cancel all remaining", + "apply": "Apply", + "cancel_event": "Cancel event", + "continue": "Continue", + "confirm": "Confirm", + "confirm_all": "Confirm all", + "disband_team": "Disband Team", + "disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you've shared this team link with will no longer be able to book using it.", + "disband_org": "Disband Organization", + "disband_org_confirmation_message": "Are you sure you want to disband this Org? All teams and members will be deleted.", + "remove_member_confirmation_message": "Are you sure you want to remove this member from the team?", + "confirm_disband_team": "Yes, disband team", + "confirm_remove_member": "Yes, remove member", + "remove_member": "Remove member", + "manage_your_team": "Manage your team", + "no_teams": "You don't have any teams yet.", + "no_teams_description": "Teams allow others to book events shared between your coworkers.", + "submit": "ដាក់ស្នើ", + "delete": "លុប", + "update": "ធ្វើបច្ចុប្បន្នភាព", + "save": "រក្សាទុក", + "pending": "កំពុងរង់ចាំ", + "open_options": "Open options", + "copy_link": "Copy link to event", + "share": "ចែករំលែក", + "share_event": "Would you mind booking my cal or send me your link?", + "copy_link_team": "Copy link to team", + "leave_team": "Leave team", + "confirm_leave_team": "Yes, leave team", + "leave_team_confirmation_message": "Are you sure you want to leave this team? You will no longer be able to book using it.", + "user_from_team": "{{user}} from {{team}}", + "preview": "Preview", + "link_copied": "Link copied!", + "private_link_copied": "Private link copied!", + "link_shared": "Link shared!", + "title": "Title", + "description": "Description", + "apps_status": "Apps Status", + "quick_video_meeting": "A quick video meeting.", + "scheduling_type": "Scheduling Type", + "preview_team": "Preview team", + "collective": "Collective", + "collective_description": "Schedule meetings when all selected team members are available.", + "duration": "Duration", + "available_durations": "Available durations", + "default_duration": "Default duration", + "default_duration_no_options": "Please choose available durations first", + "multiple_duration_mins": "{{count}} $t(minute_timeUnit)", + "minutes": "Minutes", + "round_robin": "Round Robin", + "round_robin_description": "Cycle meetings between multiple team members.", + "managed_event": "Managed Event", + "username_placeholder": "username", + "managed_event_description": "Create & distribute event types in bulk to team members", + "managed": "Managed", + "managed_event_url_clarification": "\"username\" will be filled by the username of the members assigned", + "assign_to": "Assign to", + "add_members": "Add members...", + "count_members_one": "{{count}} member", + "count_members_other": "{{count}} members", + "no_assigned_members": "No assigned members", + "assigned_to": "Assigned to", + "start_assigning_members_above": "Start assigning members above", + "locked_fields_admin_description": "Members will not be able to edit this", + "locked_fields_member_description": "This option was locked by the team admin", + "url": "URL", + "hidden": "Hidden", + "readonly": "Readonly", + "one_time_link": "One-time link", + "plan_description": "You're currently on the {{plan}} plan.", + "plan_upgrade_invitation": "Upgrade your account to the PRO plan to unlock all of the features we have to offer.", + "plan_upgrade": "You need to upgrade your plan to have more than one active event type.", + "plan_upgrade_teams": "You need to upgrade your plan to create a team.", + "plan_upgrade_instructions": "You can <1>upgrade here.", + "event_types_page_title": "ប្រភេទព្រឹត្តិការណ៍", + "event_types_page_subtitle": "បង្កើតព្រឹត្តិការណ៍ដើម្បីចែករំលែកសម្រាប់មនុស្សដើម្បីកក់នៅលើប្រតិទិនរបស់អ្នក។", + "new": "ថ្មី", + "new_event_type_btn": "ប្រភេទព្រឹត្តិការណ៍ថ្មី", + "new_event_type_heading": "បង្កើតប្រភេទព្រឹត្តិការណ៍ដំបូងរបស់អ្នក", + "new_event_type_description": "ប្រភេទព្រឹត្តិការណ៍អនុញ្ញាតឱ្យអ្នកចែករំលែកតំណដែលបង្ហាញពេលវេលាដែលមាននៅលើប្រតិទិនរបស់អ្នក និងអនុញ្ញាតឱ្យអ្នកផ្សេងធ្វើការកក់ជាមួយអ្នក។", + "event_type_created_successfully": "ប្រភេទព្រឹត្តិការណ៍ {{eventTypeTitle}} ត្រូវបានបង្កើតដោយជោគជ័យ", + "event_type_updated_successfully": "ប្រភេទព្រឹត្តិការណ៍ {{eventTypeTitle}} បានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ", + "event_type_deleted_successfully": "ប្រភេទព្រឹត្តិការណ៍ត្រូវបានលុបដោយជោគជ័យ", + "hours": "ម៉ោង", + "people": "People", + "your_email": "អ៊ីមែល​របស់​អ្នក", + "change_avatar": "ផ្លាស់ប្តូរ Avatar", + "upload_avatar": "បង្ហោះ Avatar", + "language": "ភាសា", + "timezone": "ល្វែងម៉ោង", + "first_day_of_week": "ថ្ងៃដំបូងនៃសប្តាហ៍", + "repeats_up_to_one": "ធ្វើម្តងទៀតរហូតដល់ {{count}} ដង", + "repeats_up_to_other": "ធ្វើម្តងទៀតរហូតដល់ {{count}} ដង", + "every_for_freq": "Every {{freq}} for", + "event_remaining_one": "{{count}} ព្រឹត្តិការណ៍ដែលនៅសល់", + "event_remaining_other": "{{count}} ព្រឹត្តិការណ៍ដែលនៅសល់", + "repeats_every": "ធ្វើម្តងទៀតរៀងរាល់", + "occurrence_one": "occurrence", + "occurrence_other": "occurrences", + "weekly_one": "week", + "weekly_other": "weeks", + "monthly_one": "month", + "monthly_other": "months", + "yearly_one": "year", + "yearly_other": "years", + "plus_more": "{{count}} more", + "max": "Max", + "single_theme": "Single Theme", + "brand_color": "Brand Color", + "light_brand_color": "Brand Color (Light Theme)", + "dark_brand_color": "Brand Color (Dark Theme)", + "file_not_named": "File is not named [idOrSlug]/[user]", + "create_team": "Create Team", + "name": "Name", + "create_new_team_description": "Create a new team to collaborate with users.", + "create_new_team": "Create a new team", + "open_invitations": "Open Invitations", + "new_team": "New Team", + "create_first_team_and_invite_others": "Create your first team and invite other users to work together.", + "create_team_to_get_started": "Create a team to get started", + "teams": "Teams", + "team": "Team", + "organization": "Organization", + "team_billing": "Team Billing", + "team_billing_description": "Manage billing for your team", + "upgrade_to_flexible_pro_title": "We've changed billing for teams", + "upgrade_to_flexible_pro_message": "There are members in your team without a seat. Upgrade your pro plan to cover missing seats.", + "changed_team_billing_info": "As of January 2022 we charge on a per-seat basis for team members. Members of your team who had PRO for free are now on a 14 day trial. Once their trial expires these members will be hidden from your team unless you upgrade now.", + "create_manage_teams_collaborative": "Create and manage teams to use collaborative features.", + "only_available_on_pro_plan": "This feature is only available in Pro plan", + "remove_cal_branding_description": "In order to remove the {{appName}} branding from your booking pages, you need to upgrade to a Pro account.", + "edit_profile_info_description": "Edit your profile information, which shows on your scheduling link.", + "change_email_tip": "You may need to log out and back in to see the change take effect.", + "little_something_about": "A little something about yourself.", + "profile_updated_successfully": "Profile updated successfully", + "your_user_profile_updated_successfully": "Your user profile has been updated successfully.", + "user_cannot_found_db": "User seems logged in but cannot be found in the db", + "embed_and_webhooks": "Embed & Webhooks", + "enabled": "Enabled", + "disabled": "Disabled", + "disable": "Disable", + "billing": "Billing", + "manage_your_billing_info": "Manage your billing information and cancel your subscription.", + "availability": "Availability", + "edit_availability": "Edit availability", + "configure_availability": "Configure times when you are available for bookings.", + "copy_times_to": "Copy times to", + "copy_times_to_tooltip": "Copy times to …", + "change_weekly_schedule": "Change your weekly schedule", + "logo": "Logo", + "error": "Error", + "at_least_characters_one": "Please enter at least one character", + "at_least_characters_other": "Please enter at least {{count}} characters", + "team_logo": "Team Logo", + "add_location": "Add a location", + "attendees": "Attendees", + "add_attendees": "Add attendees", + "show_advanced_settings": "Show advanced settings", + "event_name": "Event Name", + "event_name_in_calendar": "Event name in calendar", + "event_name_tooltip": "The name that will appear in calendars", + "meeting_with_user": "{Event type title} between {Organiser} & {Scheduler}", + "additional_inputs": "Additional Inputs", + "additional_input_description": "Require scheduler to input additional inputs prior the booking is confirmed", + "label": "Label", + "placeholder": "Placeholder", + "type": "Type", + "edit": "Edit", + "add_input": "Add an Input", + "disable_notes": "Hide notes in calendar", + "disable_notes_description": "For privacy reasons, additional inputs and notes will be hidden in the calendar entry. They will still be sent to your email.", + "requires_confirmation_description": "The booking needs to be manually confirmed before it is pushed to the integrations and a confirmation mail is sent.", + "recurring_event": "Recurring Event", + "recurring_event_description": "People can subscribe for recurring events", + "starting": "Starting", + "disable_guests": "Disable Guests", + "disable_guests_description": "Disable adding additional guests while booking.", + "private_link": "Generate private link", + "enable_private_url": "Enable Private URL", + "private_link_label": "Private link", + "private_link_hint": "Your private link will regenerate after each use", + "copy_private_link": "Copy private link", + "private_link_description": "Generate a private URL to share without exposing your {{appName}} username", + "invitees_can_schedule": "Invitees can schedule", + "date_range": "Date Range", + "calendar_days": "calendar days", + "business_days": "business days", + "set_address_place": "Set an address or place", + "set_link_meeting": "Set a link to the meeting", + "cal_invitee_phone_number_scheduling": "{{appName}} will ask your invitee to enter a phone number before scheduling.", + "cal_provide_google_meet_location": "{{appName}} will provide a Google Meet location.", + "cal_provide_zoom_meeting_url": "{{appName}} will provide a Zoom meeting URL.", + "cal_provide_tandem_meeting_url": "{{appName}} will provide a Tandem meeting URL.", + "cal_provide_video_meeting_url": "{{appName}} will provide a video meeting URL.", + "cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.", + "cal_provide_huddle01_meeting_url": "{{appName}} will provide a Huddle01 web3 video meeting URL.", + "cal_provide_teams_meeting_url": "{{appName}} will provide a MS Teams meeting URL. NOTE: MUST HAVE A WORK OR SCHOOL ACCOUNT", + "require_payment": "Require Payment", + "you_need_to_add_a_name": "You need to add a name", + "commission_per_transaction": "commission per transaction", + "event_type_updated_successfully_description": "Your event type has been updated successfully.", + "hide_event_type": "Hide event type", + "edit_location": "Edit location", + "into_the_future": "into the future", + "when_booked_with_less_than_notice": "When booked with less than notice", + "within_date_range": "Within a date range", + "indefinitely_into_future": "Indefinitely into the future", + "add_new_custom_input_field": "Add new custom input field", + "quick_chat": "Quick Chat", + "add_new_team_event_type": "Add a new team event type", + "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", + "minimum_booking_notice": "Minimum Notice", + "offset_toggle": "Offset start times", + "offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes", + "offset_start": "Offset by", + "offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}", + "slot_interval": "Time-slot intervals", + "slot_interval_default": "Use event length (default)", + "delete_event_type": "Delete event type?", + "delete_managed_event_type": "Delete managed event type?", + "delete_event_type_description": "Anyone who you've shared this link with will no longer be able to book using it.", + "delete_managed_event_type_description": "
  • Members assigned to this event type will also have their event types deleted.
  • Anyone who they've shared their link with will no longer be able to book using it.
", + "confirm_delete_event_type": "Yes, delete", + "delete_account": "Delete account", + "confirm_delete_account": "Yes, delete account", + "delete_account_confirmation_message": "Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.", + "integrations": "Integrations", + "apps": "Apps", + "apps_description": "Here you can find a list of your apps", + "apps_listing": "App listing", + "category_apps": "{{category}} apps", + "app_store": "App Store", + "app_store_description": "Connecting people, technology and the workplace.", + "settings": "Settings", + "event_type_moved_successfully": "Event type has been moved successfully", + "next_step_text": "Next Step", + "next_step": "Skip step", + "prev_step": "Prev step", + "install": "Install", + "installed": "Installed", + "active_install_one": "{{count}} active install", + "active_install_other": "{{count}} active installs", + "globally_install": "Globally installed", + "app_successfully_installed": "App successfully installed", + "app_could_not_be_installed": "App could not be installed", + "disconnect": "Disconnect", + "embed_your_calendar": "Embed your calendar within your webpage", + "connect_your_favourite_apps": "Connect your favourite apps.", + "automation": "Automation", + "configure_how_your_event_types_interact": "Configure how your event types should interact with your calendars.", + "toggle_calendars_conflict": "Toggle the calendars you want to check for conflicts to prevent double bookings.", + "connect_additional_calendar": "Connect additional calendar", + "calendar_updated_successfully": "Calendar updated successfully", + "conferencing": "Conferencing", + "calendar": "Calendar", + "payments": "Payments", + "not_installed": "Not installed", + "error_password_mismatch": "Passwords don't match.", + "error_required_field": "This field is required.", + "status": "Status", + "team_view_user_availability": "View user availability", + "team_view_user_availability_disabled": "User needs to accept invite to view availability", + "set_as_away": "Set yourself as away", + "set_as_free": "Disable away status", + "toggle_away_error": "Error updating away status", + "user_away": "This user is currently away.", + "user_away_description": "The person you are trying to book has set themselves to away, and therefore is not accepting new bookings.", + "meet_people_with_the_same_tokens": "Meet people with the same tokens", + "only_book_people_and_allow": "Only book and allow bookings from people who share the same tokens, DAOs, or NFTs.", + "account_created_with_identity_provider": "Your account was created using an Identity Provider.", + "account_managed_by_identity_provider": "Your account is managed by {{provider}}", + "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", + "access_token": "Access token", + "visit_roadmap": "Roadmap", + "featured_categories": "Featured Categories", + "popular_categories": "Popular Categories", + "number_apps_one": "{{count}} App", + "number_apps_other": "{{count}} Apps", + "trending_apps": "Trending Apps", + "most_popular": "Most Popular", + "installed_apps": "Installed Apps", + "free_to_use_apps": "Free", + "no_category_apps": "No {{category}} apps", + "all_apps": "All apps", + "no_category_apps_description_calendar": "Add a calendar app to check for conflicts to prevent double bookings", + "no_category_apps_description_conferencing": "Try adding a conference app for video calls with your clients", + "no_category_apps_description_payment": "Add a payment app to ease transaction between you and your clients", + "no_category_apps_description_analytics": "Add an analytics app for your booking pages", + "no_category_apps_description_automation": "Add an automation app to use", + "no_category_apps_description_other": "Add any other type of app to do all sorts of things", + "no_category_apps_description_web3": "Add a web3 app for your booking pages", + "no_category_apps_description_messaging": "Add a messaging app to set up custom notifications & reminders", + "no_category_apps_description_crm": "Add a CRM app to keep track of who you've met with", + "installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.", + "installed_app_payment_description": "Configure which payment processing services to use when charging your clients.", + "installed_app_analytics_description": "Configure which analytics apps to use for your booking pages", + "installed_app_other_description": "All your installed apps from other categories.", + "installed_app_conferencing_description": "Configure which conferencing apps to use", + "installed_app_automation_description": "Configure which automation apps to use", + "installed_app_web3_description": "Configure which web3 apps to use for your booking pages", + "installed_app_messaging_description": "Configure which messaging apps to use for setting up custom notifications & reminders", + "installed_app_crm_description": "Configure which CRM apps to use for keeping track of who you've met with", + "analytics": "Analytics", + "empty_installed_apps_headline": "No apps installed", + "empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.", + "empty_installed_apps_button": "Browse App Store", + "manage_your_connected_apps": "Manage your installed apps or change settings", + "browse_apps": "Browse Apps", + "features": "Features", + "permissions": "Permissions", + "terms_and_privacy": "Terms and Privacy", + "published_by": "Published by {{author}}", + "subscribe": "Subscribe", + "buy": "Buy", + "install_app": "Install App", + "categories": "Categories", + "pricing": "Pricing", + "learn_more": "Learn more", + "privacy_policy": "Privacy Policy", + "terms_of_service": "Terms of Service", + "remove": "Remove", + "add": "Add", + "installed_other": "{{count}} installed", + "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 <2>/auth/setup to enter a license key. If a license key is already in place, please contact <5>{{SUPPORT_MAIL_ADDRESS}} 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 to enter a license key.", + "missing_license": "Missing License", + "signup_requires": "Commercial license required", + "signup_requires_description": "{{companyName}} currently does not offer a free open source version of the sign up page. To receive full access to the signup components you need to acquire a commercial license. For personal use we recommend the Prisma Data Platform or any other Postgres interface to create accounts.", + "next_steps": "Next Steps", + "acquire_commercial_license": "Acquire a commercial license", + "the_infrastructure_plan": "The infrastructure plan is usage-based and has startup-friendly discounts.", + "prisma_studio_tip": "Create an account via Prisma Studio", + "prisma_studio_tip_description": "Learn how to set up your first user", + "contact_sales": "Contact Sales", + "error_404": "Error 404", + "default": "Default", + "set_to_default": "Set to Default", + "new_schedule_btn": "New schedule", + "add_new_schedule": "Add a new schedule", + "add_new_calendar": "Add a new calendar", + "set_calendar": "Set where to add new events to when you're booked.", + "delete_schedule": "Delete schedule", + "delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.", + "schedule_created_successfully": "{{scheduleName}} schedule created successfully", + "availability_updated_successfully": "{{scheduleName}} schedule updated successfully", + "schedule_deleted_successfully": "Schedule deleted successfully", + "default_schedule_name": "Working Hours", + "new_schedule_heading": "Create an availability schedule", + "new_schedule_description": "Creating availability schedules allows you to manage availability across event types. They can be applied to one or more event types.", + "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:", + "example_name": "John Doe", + "time_format": "ទម្រង់ពេលវេលា", + "12_hour": "12 hour", + "24_hour": "24 hour", + "12_hour_short": "12h", + "24_hour_short": "24h", + "redirect_success_booking": "Redirect on booking ", + "you_are_being_redirected": "You are being redirected to {{ url }} in $t(second, {\"count\": {{seconds}} }).", + "external_redirect_url": "https://example.com/redirect-to-my-success-page", + "redirect_url_description": "Redirect to a custom URL after a successful booking", + "duplicate": "Duplicate", + "offer_seats": "Offer seats", + "offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.", + "seats_available_one": "Seat available", + "seats_available_other": "Seats available", + "seats_nearly_full": "Seats almost full", + "seats_half_full": "Seats filling fast", + "number_of_seats": "Number of seats per booking", + "enter_number_of_seats": "Enter number of seats", + "you_can_manage_your_schedules": "You can manage your schedules on the Availability page.", + "booking_full": "No more seats available", + "api_keys": "API keys", + "api_key": "API key", + "test_api_key": "Test API key", + "test_passed": "Test passed!", + "test_failed": "Test failed", + "provide_api_key": "Provide API key", + "api_key_modal_subtitle": "API keys allow you to make API calls for your own account.", + "api_keys_subtitle": "Generate API keys to use for accessing your own account.", + "create_api_key": "Create an API key", + "personal_note": "Name this key", + "personal_note_placeholder": "E.g. Development", + "api_key_no_note": "Nameless API key", + "api_key_never_expires": "This API key has no expiration date", + "edit_api_key": "Edit API key", + "success_api_key_created": "API key created successfully", + "success_api_key_edited": "API key updated successfully", + "create": "Create", + "success_api_key_created_bold_tagline": "Save this API key somewhere safe.", + "you_will_only_view_it_once": "You will not be able to view it again once you close this modal.", + "copy_to_clipboard": "Copy to clipboard", + "enabled_after_update": "Enabled after update", + "enabled_after_update_description": "The private link will work after saving", + "confirm_delete_api_key": "Revoke this API key", + "revoke_api_key": "Revoke API key", + "api_key_copied": "API key copied!", + "api_key_expires_on":"The API key will expire on", + "delete_api_key_confirm_title": "Permanently remove this API key from your account?", + "copy": "Copy", + "expire_date": "Expiration date", + "expired": "Expired", + "never_expires": "Never expires", + "expires": "Expires", + "request_reschedule_booking": "Request to reschedule your booking", + "reason_for_reschedule": "Reason for reschedule", + "book_a_new_time": "Book a new time", + "reschedule_request_sent": "Reschedule request sent", + "reschedule_modal_description": "This will cancel the scheduled meeting, notify the scheduler and ask them to pick a new time.", + "reason_for_reschedule_request": "Reason for reschedule request", + "send_reschedule_request": "Request reschedule ", + "edit_booking": "Edit booking", + "reschedule_booking": "Reschedule booking", + "former_time": "Former time", + "confirmation_page_gif": "Add a GIF to your confirmation page", + "search": "Search", + "impersonate": "Impersonate", + "user_impersonation_heading": "User Impersonation", + "user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.", + "team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.", + "make_team_private": "Make team private", + "make_team_private_description": "Your team members won't be able to see other team members when this is turned on.", + "you_cannot_see_team_members": "You cannot see all the team members of a private team.", + "allow_booker_to_select_duration": "Allow booker to select duration", + "impersonate_user_tip": "All uses of this feature is audited.", + "impersonating_user_warning": "Impersonating username \"{{user}}\".", + "impersonating_stop_instructions": "Click here to stop", + "event_location_changed": "Updated - Your event changed the location", + "location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}", + "current_location": "Current Location", + "new_location": "New Location", + "session": "Session", + "session_description": "Control your account session", + "session_timeout_after": "Timeout session after", + "session_timeout": "Session timeout", + "session_timeout_description": "Invalidate your session after a certain amount of time.", + "no_location": "No location defined", + "set_location": "Set Location", + "update_location": "Update Location", + "location_updated": "Location updated", + "email_validation_error": "That doesn't look like an email address", + "place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.", + "create_update_react_component": "Create or update an existing React component as shown below.", + "copy_code": "Copy Code", + "code_copied": "Code copied!", + "how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?", + "choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.", + "setting_up_zapier": "Setting up your Zapier integration", + "setting_up_make": "Setting up your Make integration", + "generate_api_key": "Generate API key", + "generate_api_key_description": "Generate an API key to use with {{appName}} at", + "your_unique_api_key": "Your unique API key", + "copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.", + "zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.<1>Select Cal.com as your Trigger app. Also choose a Trigger event.<2>Choose your account and then enter your Unique API Key.<3>Test your Trigger.<4>You're set!", + "make_setup_instructions": "<0>Go to <1><0>Make Invite Link and install the Cal.com app.<1>Log into your Make account and create a new Scenario.<2>Select Cal.com as your Trigger app. Also choose a Trigger event.<3>Choose your account and then enter your Unique API Key.<4>Test your Trigger.<5>You're set!", + "install_zapier_app": "Please first install the Zapier App in the app store.", + "install_make_app": "Please first install the Make App in the app store.", + "connect_apple_server": "Connect to Apple Server", + "calendar_url": "Calendar URL", + "apple_server_generate_password": "Generate an app specific password to use with {{appName}} at", + "credentials_stored_encrypted": "Your credentials will be stored and encrypted.", + "it_stored_encrypted": "It will be stored and encrypted.", + "go_to_app_store": "Go to App Store", + "calendar_error": "Try reconnecting your calendar with all necessary permissions", + "set_your_phone_number": "Set a phone number for the meeting", + "calendar_no_busy_slots": "There are no busy slots", + "display_location_label": "Display on booking page", + "display_location_info_badge": "Location will be visible before the booking is confirmed", + "add_gif": "Add GIF", + "search_giphy": "Search Giphy", + "add_link_from_giphy": "Add link from Giphy", + "add_gif_to_confirmation": "Adding a GIF to confirmation page", + "find_gif_spice_confirmation": "Find GIF to spice up your confirmation page", + "share_feedback": "Share feedback", + "resources": "Resources", + "support_documentation": "Support Documentation", + "developer_documentation": "Developer Documentation", + "get_in_touch": "Get in touch", + "contact_support": "Contact Support", + "feedback": "Feedback", + "submitted_feedback": "Thank you for your feedback!", + "feedback_error": "Error sending feedback", + "comments": "Share your comments here:", + "booking_details": "Booking details", + "or_lowercase": "or", + "nevermind": "Nevermind", + "go_to": "Go to: ", + "zapier_invite_link": "Zapier Invite Link", + "meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.", + "dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee", + "event_location": "Event's location", + "reschedule_optional": "Reason for rescheduling (optional)", + "reschedule_placeholder": "Let others know why you need to reschedule", + "event_cancelled": "This event is canceled", + "emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.", + "this_input_will_shown_booking_this_event": "This input will be shown when booking this event", + "meeting_url_in_confirmation_email": "Meeting url is in the confirmation email", + "url_start_with_https": "URL needs to start with http:// or https://", + "number_provided": "Phone number will be provided", + "before_event_trigger": "before event starts", + "event_cancelled_trigger": "when event is canceled", + "new_event_trigger": "when new event is booked", + "email_host_action": "send email to host", + "email_attendee_action": "send email to attendees", + "sms_attendee_action": "Send SMS to attendee", + "sms_number_action": "send SMS to a specific number", + "send_reminder_sms": "Easily send meeting reminders via SMS to your attendees", + "whatsapp_number_action": "send WhatsApp message to a specific number", + "whatsapp_attendee_action": "send WhatsApp message to attendee", + "workflows": "Workflows", + "new_workflow_btn": "New Workflow", + "add_new_workflow": "Add a new workflow", + "reschedule_event_trigger": "when event is rescheduled", + "trigger": "Trigger", + "triggers": "Triggers", + "action": "Action", + "workflows_to_automate_notifications": "Create workflows to automate notifications and reminders", + "workflow_name": "Workflow name", + "custom_workflow": "Custom workflow", + "workflow_created_successfully": "{{workflowName}} created successfully", + "delete_workflow_description": "Are you sure you want to delete this workflow?", + "delete_workflow": "Delete Workflow", + "confirm_delete_workflow": "Yes, delete workflow", + "workflow_deleted_successfully": "Workflow deleted successfully", + "how_long_before": "How long before event starts?", + "day_timeUnit": "days", + "hour_timeUnit": "hours", + "minute_timeUnit": "mins", + "new_workflow_heading": "Create your first workflow", + "new_workflow_description": "Workflows enable you to automate sending reminders and notifications.", + "active_on": "Active on", + "workflow_updated_successfully": "{{workflowName}} workflow updated successfully", + "premium_to_standard_username_description": "This is a standard username and updating will take you to billing to downgrade.", + "current": "Current", + "premium": "premium", + "standard": "standard", + "confirm_username_change_dialog_title": "Confirm username change", + "change_username_standard_to_premium": "As you are changing from a standard to a premium username, you will be taken to the checkout to upgrade.", + "change_username_premium_to_standard": "As you are changing from a premium to a standard username, you will be taken to the checkout to downgrade.", + "go_to_stripe_billing": "Go to billing", + "stripe_description": "Require payment for bookings (0.5% + €0.10 commission per transaction)", + "trial_expired": "Your trial has expired", + "remove_app": "Remove App", + "yes_remove_app": "Yes, remove app", + "are_you_sure_you_want_to_remove_this_app": "Are you sure you want to remove this app?", + "app_removed_successfully": "App removed successfully", + "error_removing_app": "Error removing app", + "web_conference": "Web conference", + "requires_confirmation": "Requires confirmation", + "always_requires_confirmation": "Always", + "requires_confirmation_threshold": "Requires confirmation if booked with < {{time}} $t({{unit}}_timeUnit) notice", + "may_require_confirmation": "May require confirmation", + "nr_event_type_one": "{{count}} event type", + "nr_event_type_other": "{{count}} event types", + "add_action": "Add action", + "set_whereby_link": "Set Whereby link", + "invalid_whereby_link": "Please enter a valid Whereby Link", + "set_around_link": "Set Around.Co link", + "invalid_around_link": "Please enter a valid Around Link", + "set_riverside_link": "Set Riverside link", + "invalid_riverside_link": "Please enter a valid Riverside Link", + "invalid_ping_link": "Please enter a valid Ping.gg Link", + "add_exchange2013": "Connect Exchange 2013 Server", + "add_exchange2016": "Connect Exchange 2016 Server", + "custom_template": "Custom template", + "email_body": "Email body", + "text_message": "Text message", + "specific_issue": "Have a specific issue?", + "browse_our_docs": "browse our docs", + "choose_template": "Choose a template", + "custom": "Custom", + "reminder": "Reminder", + "rescheduled": "Rescheduled", + "completed": "Completed", + "reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}", + "not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.", + "minute_one": "{{count}} minute", + "minute_other": "{{count}} minutes", + "hour_one": "{{count}} hour", + "hour_other": "{{count}} hours", + "invalid_input": "Invalid input", + "broken_video_action": "We could not add the <1>{{location}} meeting link to your scheduled event. Contact your invitees or update your calendar event to add the details. You can either <3> change your location on the event type or try <5>removing and adding the app again.", + "broken_calendar_action": "We could not update your <1>{{calendar}}. <2> Please check your calendar settings or remove and add your calendar again ", + "attendee_name": "Attendee's name", + "scheduler_full_name": "The full name of the person booking", + "broken_integration": "Broken integration", + "problem_adding_video_link": "There was a problem adding a video link", + "problem_updating_calendar": "There was a problem updating your calendar", + "active_on_event_types_one": "Active on {{count}} event type", + "active_on_event_types_other": "Active on {{count}} event types", + "no_active_event_types": "No active event types", + "new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}", + "new_seat_title": "Someone has added themselves to an event", + "variable": "Variable", + "event_name_variable": "Event name", + "attendee_name_variable": "Attendee", + "event_date_variable": "Event date", + "event_time_variable": "Event time", + "timezone_variable": "Timezone", + "location_variable": "Location", + "additional_notes_variable": "Additional notes", + "organizer_name_variable": "Organizer name", + "app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.", + "invalid_number": "Invalid phone number", + "navigate": "Navigate", + "open": "Open", + "close": "Close", + "upgrade": "Upgrade", + "upgrade_to_access_recordings_title": "Upgrade to access recordings", + "upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls", + "recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan", + "team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.", + "team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.", + "show_eventtype_on_profile": "Show on Profile", + "embed": "Embed", + "new_username": "New username", + "current_username": "Current username", + "example_1": "Example 1", + "example_2": "Example 2", + "booking_question_identifier": "Booking Question Identifier", + "company_size": "Company size", + "what_help_needed": "What do you need help with?", + "variable_format": "Variable format", + "webhook_subscriber_url_reserved": "Webhook subscriber url is already defined", + "custom_input_as_variable_info": "Ignore all special characters of the additional input label (use only letters and numbers), use uppercase for all letters and replace whitespaces with underscores.", + "using_booking_questions_as_variables": "How do I use booking questions as variables?", + "download_desktop_app": "Download desktop app", + "set_ping_link": "Set Ping link", + "rate_limit_exceeded": "Rate limit exceeded", + "when_something_happens": "When something happens", + "action_is_performed": "An action is performed", + "test_action": "Test action", + "notification_sent": "Notification sent", + "no_input": "No input", + "test_workflow_action": "Test workflow action", + "send_sms": "Send SMS", + "send_sms_to_number": "Are you sure you want to send a SMS to {{number}}?", + "missing_connected_calendar": "No default calendar connected", + "connect_your_calendar_and_link": "You can connect your calendar from <1>here.", + "default_calendar_selected": "Default calendar", + "hide_from_profile": "Hide from profile", + "event_setup_tab_title": "Event Setup", + "event_limit_tab_title": "Limits", + "event_limit_tab_description": "How often you can be booked", + "event_advanced_tab_description": "Calendar settings & more...", + "event_advanced_tab_title": "Advanced", + "event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.", + "event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.", + "event_setup_booking_limits_error": "Booking limits must be in ascending order. [day, week, month, year]", + "event_setup_duration_limits_error": "Duration limits must be in ascending order. [day, week, month, year]", + "select_which_cal": "Select which calendar to add bookings to", + "custom_event_name": "Custom event name", + "custom_event_name_description": "Create customised event names to display on calendar event", + "2fa_required": "Two factor authentication required", + "incorrect_2fa": "Incorrect two factor authentication code", + "which_event_type_apply": "Which event type will this apply to?", + "no_workflows_description": "Workflows enable simple automation to send notifications & reminders enabling you to build processes around your events.", + "timeformat_profile_hint": "នេះគឺជាការកំណត់ខាងក្នុង ហើយនឹងមិនប៉ះពាល់ដល់របៀបដែលពេលវេលាត្រូវបានបង្ហាញនៅលើទំព័រកក់សាធារណៈសម្រាប់អ្នក ឬនរណាម្នាក់ដែលកក់អ្នក។", + "create_workflow": "Create a workflow", + "do_this": "Do this", + "turn_off": "Turn off", + "turn_on": "Turn on", + "settings_updated_successfully": "Settings updated successfully", + "error_updating_settings": "Error updating settings", + "personal_cal_url": "My personal {{appName}} URL", + "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.", + "bio":"Bio", + "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", + "start_of_week": "ការចាប់ផ្តើមនៃសប្តាហ៍", + "recordings_title": "Recordings", + "recording": "Recording", + "happy_scheduling": "Happy scheduling", + "select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.", + "check_for_conflicts": "Check for conflicts", + "view_recordings": "View recordings", + "adding_events_to": "Adding events to", + "follow_system_preferences": "Follow system preferences", + "custom_brand_colors": "Custom brand colors", + "customize_your_brand_colors": "Customize your own brand colour into your booking page.", + "pro": "Pro", + "removes_cal_branding": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}.'", + "profile_picture": "Profile Picture", + "upload": "Upload", + "add_profile_photo": "Add profile photo", + "web3": "Web3", + "token_address": "Token Address", + "blockchain": "Blockchain", + "old_password": "Old password", + "secure_password": "Your new super secure password", + "error_updating_password": "Error updating password", + "two_factor_auth": "Two factor authentication", + "recurring_event_tab_description": "Set up a repeating schedule", + "today": "ថ្ងៃនេះ", + "appearance": "រូបរាង", + "my_account": "គណនី​របស់ខ្ញុំ", + "general": "ទូទៅ", + "calendars": "ប្រតិទិន", + "2fa_auth": "Two factor auth", + "invoices": "វិក្កយបត្រ", + "embeds": "Embeds", + "impersonation": "Impersonation", + "impersonation_description": "Settings to manage user impersonation", + "users": "អ្នកប្រើប្រាស់", + "user": "អ្នកប្រើប្រាស់", + "profile_description": "Manage settings for your {{appName}} profile", + "users_description": "Here you can find a list of all users", + "users_listing": "User listing", + "general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។", + "calendars_description": "Configure how your event types interact with your calendars", + "appearance_description": "Manage settings for your booking appearance", + "conferencing_description": "Add your favourite video conferencing apps for your meetings", + "add_conferencing_app": "Add Conferencing App", + "password_description": "Manage settings for your account passwords", + "set_up_two_factor_authentication": "Set up your Two-factor authentication", + "we_just_need_basic_info": "We just need some basic info to get your profile setup.", + "skip": "Skip", + "do_this_later": "Do this later", + "set_availability_getting_started_subtitle_1": "Define ranges of time when you are available", + "set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.", + "connect_calendars_from_app_store": "You can add more calendars from the app store", + "connect_conference_apps": "Connect conference apps", + "connect_calendar_apps": "Connect calendar apps", + "connect_payment_apps": "Connect payment apps", + "connect_automation_apps": "Connect automation apps", + "connect_analytics_apps": "Connect analytics apps", + "connect_other_apps": "Connect other apps", + "connect_web3_apps": "Connect web3 apps", + "connect_messaging_apps": "Connect messaging apps", + "connect_crm_apps": "Connect CRM apps", + "current_step_of_total": "Step {{currentStep}} of {{maxSteps}}", + "add_variable": "Add variable", + "custom_phone_number": "Custom phone number", + "message_template": "Message template", + "email_subject": "Email subject", + "add_dynamic_variables": "Add dynamic text variables", + "event_name_info": "The event type name", + "event_date_info": "The event date", + "event_time_info": "The event start time", + "location_info": "The location of the event", + "additional_notes_info": "The additional notes of booking", + "attendee_name_info": "The person booking's name", + "organizer_name_info": "Organizer’s name", + "to": "To", + "workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully", + "download_responses": "Download responses", + "download_responses_description": "Download all responses to your form in CSV format.", + "download": "Download", + "download_recording": "Download Recording", + "recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download", + "create_your_first_form": "Create your first form", + "create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.", + "create_your_first_webhook": "Create your first Webhook", + "create_your_first_webhook_description": "With Webhooks you can receive meeting data in real-time when something happens in {{appName}}.", + "for_a_maximum_of": "For a maximum of", + "event_one": "event", + "event_other": "events", + "profile_team_description": "Manage settings for your team profile", + "profile_org_description": "Manage settings for your organization profile", + "members_team_description": "Users that are in the group", + "organization_description": "Manage the admins and members in your organization", + "team_url": "Team URL", + "team_members": "Team members", + "more": "More", + "more_page_footer": "We view the mobile application as an extension of the web application. If you are performing any complicated actions, please refer back to the web application.", + "workflow_example_1": "Send SMS reminder 24 hours before event starts to attendee", + "workflow_example_2": "Send custom SMS when event is rescheduled to attendee", + "workflow_example_3": "Send custom email when new event is booked to host", + "workflow_example_4": "Send email reminder 1 hour before events starts to attendee", + "workflow_example_5": "Send custom email when event is rescheduled to host", + "workflow_example_6": "Send custom SMS when new event is booked to host", + "welcome_to_cal_header": "Welcome to {{appName}}!", + "edit_form_later_subtitle": "You’ll be able to edit this later.", + "connect_calendar_later": "I'll connect my calendar later", + "problem_saving_user_profile": "There was a problem saving your data. Please try again or reach out to customer support.", + "purchase_missing_seats": "Purchase missing seats", + "slot_length": "Slot length", + "booking_appearance": "Booking Appearance", + "appearance_team_description": "Manage settings for your team's booking appearance", + "only_owner_change": "Only the owner of this team can make changes to the team's booking ", + "team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'", + "invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}", + "token_invalid_expired": "Token is either invalid or expired.", + "exchange_add": "Connect to Microsoft Exchange", + "exchange_authentication": "Authentication method", + "exchange_authentication_standard": "Basic authentication", + "exchange_authentication_ntlm": "NTLM authentication", + "exchange_compression": "GZip compression", + "exchange_version": "Exchange Version", + "exchange_version_2007_SP1": "2007 SP1", + "exchange_version_2010": "2010", + "exchange_version_2010_SP1": "2010 SP1", + "exchange_version_2010_SP2": "2010 SP2", + "exchange_version_2013": "2013", + "exchange_version_2013_SP1": "2013 SP1", + "exchange_version_2015": "2015", + "exchange_version_2016": "2016", + "routing_forms_description": "Create forms to direct attendees to the correct destinations", + "routing_forms_send_email_owner": "Send Email to Owner", + "routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted", + "add_new_form": "Add new form", + "add_new_team_form": "Add new form to your team", + "create_your_first_route": "Create your first route", + "route_to_the_right_person": "Route to the right person based on the answers to your form", + "form_description": "Create your form to route a booker", + "copy_link_to_form": "Copy link to form", + "theme": "Theme", + "theme_applies_note": "This only applies to your public booking pages", + "theme_system": "System default", + "add_a_team": "Add a team", + "add_webhook_description": "Receive meeting data in real-time when something happens in {{appName}}", + "triggers_when": "Triggers when", + "test_webhook": "Please ping test before creating.", + "enable_webhook": "Enable Webhook", + "add_webhook": "Add Webhook", + "webhook_edited_successfully": "Webhook saved", + "api_keys_description": "Generate API keys for accessing your own account", + "new_api_key": "New API key", + "active": "active", + "api_key_updated": "API key name updated", + "api_key_update_failed": "Error updating API key name", + "embeds_title": "HTML iframe embed", + "embeds_description": "Embed all your event types on your website", + "create_first_api_key": "Create your first API key", + "create_first_api_key_description": "API keys allow other apps to communicate with {{appName}}", + "back_to_signin": "Back to sign in", + "reset_link_sent": "Reset link sent", + "password_reset_email": "An email is on it’s way to {{email}} with instructions to reset your password.", + "password_reset_leading": "If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.", + "password_updated": "Password updated!", + "pending_payment": "Pending payment", + "pending_invites": "Pending Invites", + "not_on_cal": "Not on {{appName}}", + "no_calendar_installed": "No calendar installed", + "no_calendar_installed_description": "You have not yet connected any of your calendars", + "add_a_calendar": "Add a calendar", + "change_email_hint": "You may need to log out and back in to see any change take effect", + "confirm_password_change_email": "Please confirm your password before changing your email address", + "seats": "seats", + "every_app_published": "Every app published on the {{appName}} App Store is open source and thoroughly tested via peer reviews. Nevertheless, {{companyName}} does not endorse or certify these apps unless they are published by {{appName}}. If you encounter inappropriate content or behaviour please report it.", + "report_app": "Report app", + "limit_booking_frequency": "Limit booking frequency", + "limit_booking_frequency_description": "Limit how many times this event can be booked", + "limit_total_booking_duration": "Limit total booking duration", + "limit_total_booking_duration_description": "Limit total amount of time that this event can be booked", + "add_limit": "Add Limit", + "team_name_required": "Team name required", + "show_attendees": "Share attendee information between guests", + "show_available_seats_count": "Show the number of available seats", + "how_booking_questions_as_variables": "How to use booking questions as variables?", + "format": "Format", + "uppercase_for_letters": "Use uppercase for all letters", + "replace_whitespaces_underscores": "Replace whitespaces with underscores", + "manage_billing": "Manage billing", + "manage_billing_description": "Manage all things billing", + "billing_freeplan_title": "You're currently on the FREE plan", + "billing_freeplan_description": "We work better in teams. Extend your workflows with round-robin and collective events and make advanced routing forms", + "billing_freeplan_cta": "Try now", + "billing_portal": "Billing portal", + "billing_help_cta": "Contact support", + "ignore_special_characters_booking_questions": "Ignore special characters in your booking question identifier. Use only letters and numbers", + "retry": "Retry", + "fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again or reach out to customer support.", + "calendar_connection_fail": "Calendar connection failed", + "booking_confirmation_success": "Booking confirmation succeeded", + "booking_rejection_success": "Booking rejection succeeded", + "booking_tentative": "This booking is tentative", + "booking_accept_intent": "Oops, I want to accept", + "we_wont_show_again": "We won't show this again", + "couldnt_update_timezone": "We couldn't update the timezone", + "updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}", + "update_timezone": "Update timezone", + "update_timezone_question": "Update Timezone?", + "update_timezone_description": "It seems like your local timezone has changed to {{formattedCurrentTz}}. It's very important to have the correct timezone to prevent bookings at undesired times. Do you want to update it?", + "dont_update": "Don't update", + "require_additional_notes": "Require additional notes", + "require_additional_notes_description": "Require additional notes to be filled out when booking", + "email_address_action": "send email to a specific email address", + "after_event_trigger": "after event ends", + "how_long_after": "How long after event ends?", + "no_available_slots": "No Available slots", + "time_available": "Time available", + "cant_find_the_right_video_app_visit_our_app_store": "Can't find the right video app? Visit our <1>App Store.", + "install_new_calendar_app": "Install new calendar app", + "make_phone_number_required": "Make phone number required for booking event", + "new_event_type_availability": "{{eventTypeTitle}} Availability", + "error_editing_availability": "Error editing availability", + "dont_have_permission": "You don't have permission to access this resource.", + "saml_config": "SAML", + "saml_configuration_placeholder": "Please paste the SAML metadata from your Identity Provider here", + "saml_email_required": "Please enter an email so we can find your SAML Identity Provider", + "saml_sp_title": "Service Provider Details", + "saml_sp_description": "Your Identity Provider (IdP) will ask you for the following details to complete the SAML application configuration.", + "saml_sp_acs_url": "ACS URL", + "saml_sp_entity_id": "SP Entity ID", + "saml_sp_acs_url_copied": "ACS URL copied!", + "saml_sp_entity_id_copied": "SP Entity ID copied!", + "add_calendar": "Add Calendar", + "limit_future_bookings": "Limit future bookings", + "limit_future_bookings_description": "Limit how far in the future this event can be booked", + "no_event_types": "No event types setup", + "no_event_types_description": "{{name}} has not setup any event types for you to book.", + "billing_frequency": "Billing Frequency", + "monthly": "Monthly", + "yearly": "Yearly", + "checkout": "Checkout", + "your_team_disbanded_successfully": "Your team has been disbanded successfully", + "your_org_disbanded_successfully": "Your organization has been disbanded successfully", + "error_creating_team": "Error creating team", + "you": "You", + "resend_email": "Resend email", + "member_already_invited": "Member has already been invited", + "enter_email_or_username": "Enter an email or username", + "team_name_taken": "This name is already taken", + "must_enter_team_name": "Must enter a team name", + "team_url_required": "Must enter a team URL", + "url_taken": "This URL is already taken", + "problem_registering_domain": "There was a problem with registering the subdomain, please try again or contact an administrator", + "team_publish": "Publish team", + "number_text_notifications": "Phone number (Text notifications)", + "number_sms_notifications": "Phone number (SMS notifications)", + "attendee_email_variable": "Attendee email", + "attendee_email_info": "The person booking's email", + "kbar_search_placeholder": "Type a command or search...", + "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", + "reschedule_reason": "Reschedule reason", + "choose_common_schedule_team_event": "Choose a common schedule", + "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", + "reason": "Reason", + "sender_id": "Sender ID", + "sender_id_error_message": "Only letters, numbers and spaces allowed (max. 11 characters)", + "test_routing_form": "Test Routing Form", + "test_preview": "Test Preview", + "route_to": "Route to", + "test_preview_description": "Test your routing form without submitting any data", + "test_routing": "Test Routing", + "payment_app_disabled": "An admin has disabled a payment app", + "edit_event_type": "Edit event type", + "collective_scheduling": "Collective Scheduling", + "make_it_easy_to_book": "Make it easy to book your team when everyone is available.", + "find_the_best_person": "Find the best person available and cycle through your team.", + "fixed_round_robin": "Fixed round robin", + "add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.", + "calcom_is_better_with_team": "{{appName}} is better with teams", + "the_calcom_team": "The {{companyName}} team", + "add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.", + "booking_limit_reached": "Booking Limit for this event type has been reached", + "duration_limit_reached": "Duration Limit for this event type has been reached", + "admin_has_disabled": "An admin has disabled {{appName}}", + "disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}", + "event_replaced_notice": "An admin has replaced one of your event types", + "email_subject_slug_replacement": "A team administrator has replaced your event /{{slug}}", + "email_body_slug_replacement_notice": "An administrator on the {{teamName}} team has replaced your event type /{{slug}} with a managed event type that they control.", + "email_body_slug_replacement_info": "Your link will continue to work but some settings for it may have changed. You can review it in event types.", + "email_body_slug_replacement_suggestion": "If you have any questions about the event type, please reach out to your administrator.

Happy scheduling,
The Cal.com team", + "disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.", + "payment_disabled_still_able_to_book": "Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin reenables your payment method.", + "app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.", + "app_disabled_video": "The admin has disabled {{appName}} which may affect your event types. If you have event types with {{appName}} as the location then it will default to Cal Video.", + "app_disabled_subject": "{{appName}} has been disabled", + "navigate_installed_apps": "Go to installed apps", + "disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.", + "enable_apps": "Enable Apps", + "enable_apps_description": "Enable apps that users can integrate with {{appName}}", + "purchase_license": "Purchase a License", + "already_have_key": "I already have a key:", + "already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.", + "app_is_enabled": "{{appName}} is enabled", + "app_is_disabled": "{{appName}} is disabled", + "keys_have_been_saved": "Keys have been saved", + "disable_app": "Disable App", + "disable_app_description": "Disabling this app could cause problems with how your users interact with Cal", + "edit_keys": "Edit Keys", + "admin_apps_description": "Enable apps for your instance of Cal", + "no_available_apps": "There are no available apps", + "no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'", + "no_apps": "There are no apps enabled in this instance of Cal", + "no_apps_configured": "No app has been configured yet", + "enable_in_settings": "You can enable apps in the settings", + "please_contact_admin": "Please contact your admin", + "apps_settings": "Apps settings", + "fill_this_field": "Please fill in this field", + "options": "Options", + "enter_option": "Enter Option {{index}}", + "add_an_option": "Add an option", + "radio": "Radio", + "google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar", + "individual": "Individual", + "all_bookings_filter_label": "All Bookings", + "all_users_filter_label": "All Users", + "your_bookings_filter_label": "Your Bookings", + "meeting_url_variable": "Meeting url", + "meeting_url_info": "The event meeting conference url", + "date_overrides": "Date overrides", + "date_overrides_subtitle": "Add dates when your availability changes from your daily hours.", + "date_overrides_info": "Date overrides are archived automatically after the date has passed", + "date_overrides_dialog_which_hours": "Which hours are you free?", + "date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?", + "date_overrides_dialog_title": "Select the dates to override", + "date_overrides_unavailable": "Unavailable all day", + "date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)", + "date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates", + "date_overrides_add_btn": "Add Override", + "date_overrides_update_btn": "Update Override", + "date_successfully_added": "Date override added successfully", + "event_type_duplicate_copy_text": "{{slug}}-copy", + "set_as_default": "Set as default", + "hide_eventtype_details": "Hide event type details", + "show_navigation": "Show navigation", + "hide_navigation": "Hide navigation", + "verification_code_sent": "Verification code sent", + "verified_successfully": "Verified successfully", + "wrong_code": "Wong verification code", + "not_verified": "Not yet verified", + "no_availability_in_month": "No availability in {{month}}", + "view_next_month": "View next month", + "send_code": "Send code", + "number_verified": "Number Verified", + "create_your_first_team_webhook_description": "Create your first webhook for this team event type", + "create_webhook_team_event_type": "Create a webhook for this team event type", + "disable_success_page": "Disable Success Page (only works if you have a redirect URL)", + "invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet", + "change_password_admin": "Change Password to gain admin access", + "username_already_taken": "Username is already taken", + "assignment": "Assignment", + "fixed_hosts": "Fixed Hosts", + "add_fixed_hosts": "Add fixed hosts", + "round_robin_hosts": "Round-Robin Hosts", + "minimum_round_robin_hosts_count": "Number of hosts required to attend", + "hosts": "Hosts", + "upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.", + "orgs_upgrade_to_enable_feature" : "You need to upgrade to our enterprise plan to enable this feature.", + "new_attendee": "New Attendee", + "awaiting_approval": "Awaiting Approval", + "requires_google_calendar": "This app requires a Google Calendar connection", + "connected_google_calendar": "You have connected a Google Calendar account.", + "using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar", + "continue_to_install_google_calendar": "Continue to install Google Calendar", + "install_google_meet": "Install Google Meet", + "install_google_calendar": "Install Google Calendar", + "sender_name": "Sender name", + "already_invited": "Attendee already invited", + "no_recordings_found": "No recordings found", + "new_workflow_subtitle": "New workflow for...", + "reporting": "Reporting", + "reporting_feature": "See all incoming form data and download it as a CSV", + "teams_plan_required": "Teams plan required", + "routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.", + "choose_a_license": "Choose a license", + "choose_license_description": "Cal.com comes with an accessible and free AGPLv3 license which has limitations. We are onboarding Enterprise customers for the commercial license which you can inquire about by contacting sales below.", + "license": "License", + "agplv3_license": "AGPLv3 License", + "no_need_to_keep_your_code_open_source": "No need to keep your code open source", + "repackage_rebrand_resell": "Repackage, rebrand and resell easily", + "a_vast_suite_of_enterprise_features": "A vast suite of enterprise features", + "free_license_fee": "$0.00/month", + "forever_open_and_free": "Forever Open & Free", + "required_to_keep_your_code_open_source": "Required to keep your code open source", + "cannot_repackage_and_resell": "Cannot repackage, rebrand and resell easily", + "no_enterprise_features": "No enterprise features", + "step_enterprise_license": "Enterprise License", + "step_enterprise_license_description": "Everything for a commercial use case with private hosting, repackaging, rebranding and reselling and access exclusive enterprise components.", + "setup": "Setup", + "setup_description": "Setup Cal.com instance", + "configure": "Configure", + "sso_configuration": "Single Sign-On", + "sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider", + "sso_oidc_heading": "SSO with OIDC", + "sso_oidc_description": "Configure OIDC SSO with Identity Provider of your choice.", + "sso_oidc_configuration_title": "OIDC Configuration", + "sso_oidc_configuration_description": "Configure OIDC connection to your identity provider. You can find the required information in your identity provider.", + "sso_oidc_callback_copied": "Callback URL copied", + "sso_saml_heading": "SSO with SAML", + "sso_saml_description": "Configure SAML SSO with Identity Provider of your choice.", + "sso_saml_configuration_title": "SAML Configuration", + "sso_saml_configuration_description": "Configure SAML connection to your identity provider. You can find the required information in your identity provider.", + "sso_saml_acsurl_copied": "ACS URL copied", + "sso_saml_entityid_copied": "Entity ID copied", + "sso_connection_created_successfully": "{{connectionType}} configuration created successfully", + "sso_connection_deleted_successfully": "{{connectionType}} configuration deleted successfully", + "delete_sso_configuration": "Delete {{connectionType}} configuration", + "delete_sso_configuration_confirmation": "Yes, delete {{connectionType}} configuration", + "delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.", + "organizer_timezone": "Organizer timezone", + "email_user_cta": "View Invitation", + "email_no_user_invite_heading_team": "You’ve been invited to join a {{appName}} team", + "email_no_user_invite_heading_org": "You’ve been invited to join a {{appName}} organization", + "email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", + "email_user_invite_subheading_team": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", + "email_user_invite_subheading_org": "{{invitedBy}} has invited you to join their organization `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your organization to schedule meetings without the email tennis.", + "email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your {{entity}} in no time.", + "email_no_user_step_one": "Choose your username", + "email_no_user_step_two": "Connect your calendar account", + "email_no_user_step_three": "Set your Availability", + "email_no_user_step_four": "Join {{teamName}}", + "email_no_user_signoff": "Happy Scheduling from the {{appName}} team", + "impersonation_user_tip": "You are about to impersonate a user, which means you can make changes on their behalf. Please be careful.", + "available_variables": "Available variables", + "scheduler": "{Scheduler}", + "no_workflows": "No workflows", + "change_filter": "Change filter to see your personal and team workflows.", + "change_filter_common": "Change filter to see the results.", + "no_results_for_filter": "No results for the filter", + "recommended_next_steps": "Recommended next steps", + "create_a_managed_event": "Create a managed event type", + "meetings_are_better_with_the_right": "Meetings are better with the right team members there. Invite them now.", + "create_a_one_one_template": "Create a one-one one template for an event type and distribute it to multiple members.", + "collective_or_roundrobin": "Collective or round-robin", + "book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.", + "event_no_longer_attending_subject": "No longer attending {{title}} at {{date}}", + "no_longer_attending": "You are no longer attending this event", + "attendee_no_longer_attending_subject": "An attendee is no longer attending {{title}} at {{date}}", + "attendee_no_longer_attending": "An attendee is no longer attending your event", + "attendee_no_longer_attending_subtitle": "{{name}} has canceled. This means a seat has opened up for this time slot", + "create_event_on": "Create event on", + "create_routing_form_on": "Create routing form on", + "default_app_link_title": "Set a default app link", + "default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.", + "organizer_default_conferencing_app": "Organizer's default app", + "under_maintenance": "Down for maintenance", + "under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.", + "event_type_seats": "{{numberOfSeats}} seats", + "booking_questions_title": "Booking questions", + "booking_questions_description": "Customize the questions asked on the booking page", + "add_a_booking_question": "Add a question", + "identifier": "Identifier", + "duplicate_email": "Email is duplicate", + "booking_with_payment_cancelled": "Paying for this event is no longer possible", + "booking_with_payment_cancelled_already_paid": "A refund for this booking payment is on its way.", + "booking_with_payment_cancelled_refunded": "This booking payment has been refunded.", + "booking_confirmation_failed": "Booking confirmation failed", + "not_enough_seats": "Not enough seats", + "form_builder_field_already_exists": "A field with this name already exists", + "show_on_booking_page": "Show on booking page", + "get_started_zapier_templates": "Get started with Zapier templates", + "team_is_unpublished": "{{team}} is unpublished", + "org_is_unpublished_description": "This organization link is currently not available. Please contact the organization owner or ask them to publish it.", + "team_is_unpublished_description": "This team link is currently not available. Please contact the team owner or ask them to publish it.", + "team_member": "Team member", + "a_routing_form": "A Routing Form", + "form_description_placeholder": "Form Description", + "keep_me_connected_with_form": "Keep me connected with the form", + "fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.", + "form_deleted": "Form deleted", + "delete_form": "Are you sure you want to delete this form?", + "delete_form_action": "Yes, delete Form", + "delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.", + "delete_form_confirmation_2": "All associated responses will be deleted.", + "typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.", + "modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.", + "connected_forms": "Connected Forms", + "form_modifications_warning": "Following forms would be affected when you modify fields or routes here.", + "responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.", + "this_is_what_your_users_would_see": "This is what your users would see", + "identifies_name_field": "Identifies field by this name.", + "add_1_option_per_line": "Add 1 option per line", + "select_a_router": "Select a router", + "add_a_new_route": "Add a new Route", + "make_informed_decisions": "Make informed decisions with Insights", + "make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.", + "view_bookings_across": "View bookings across all members", + "view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team", + "identify_booking_trends": "Identify booking trends", + "identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers", + "spot_popular_event_types": "Spot popular event types", + "spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings", + "no_responses_yet": "No responses yet", + "no_routes_defined": "No routes defined", + "this_will_be_the_placeholder": "This will be the placeholder", + "error_booking_event": "An error occurred when booking the event, please refresh the page and try again", + "timeslot_missing_title": "No timeslot selected", + "timeslot_missing_description": "Please select a timeslot to book the event.", + "timeslot_missing_cta": "Select timeslot", + "switch_monthly": "Switch to monthly view", + "switch_weekly": "Switch to weekly view", + "switch_multiday": "Switch to day view", + "switch_columnview": "Switch to column view", + "num_locations": "{{num}} location options", + "select_on_next_step": "Select on the next step", + "this_meeting_has_not_started_yet": "This meeting has not started yet", + "this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account", + "connect_app": "Connect {{dependencyName}}", + "app_is_connected": "{{dependencyName}} is connected", + "requires_app": "Requires {{dependencyName}}", + "verification_code": "Verification code", + "can_you_try_again": "Can you try again with a different time?", + "verify": "Verify", + "timezone_info": "The timezone of the person receiving", + "event_end_time_variable": "Event end time", + "event_end_time_info": "The event end time", + "cancel_url_variable": "Cancel URL", + "cancel_url_info": "The URL to cancel the booking", + "reschedule_url_variable": "Reschedule URL", + "reschedule_url_info": "The URL to reschedule the booking", + "invalid_event_name_variables": "There is an invalid variable in your event name", + "select_all": "Select All", + "default_conferencing_bulk_title": "Bulk update existing event types", + "members_default_schedule": "Member's default schedule", + "set_by_admin": "Set by team admin", + "members_default_location": "Member's default location", + "members_default_schedule_description": "We will use each members default availability schedule. They will be able to edit or change it.", + "requires_at_least_one_schedule": "You are required to have at least one schedule", + "default_conferencing_bulk_description": "Update the locations for the selected event types", + "locked_for_members": "Locked for members", + "locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings", + "locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings", + "locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings", + "locked_by_admin": "Locked by team admin", + "app_not_connected": "You have not connected a {{appName}} account.", + "connect_now": "Connect now", + "managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member", + "managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members", + "managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?", + "managed_event_dialog_title_other": "The url /{{slug}} already exists for {{count}} members. Do you want to replace it?", + "managed_event_dialog_information_one": "{{names}} is already using the /{{slug}} url.", + "managed_event_dialog_information_other": "{{names}} are already using the /{{slug}} url.", + "managed_event_dialog_clarification": "If you choose to replace it, we will notify them. Go back and remove them if you don't want to overwrite it.", + "review_event_type": "Review Event Type", + "looking_for_more_analytics": "Looking for more analytics?", + "looking_for_more_insights": "Looking for more Insights?", + "add_filter": "Add filter", + "select_user": "Select User", + "select_event_type": "Select Event Type", + "select_date_range": "Select Date Range", + "popular_events": "Popular Events", + "no_event_types_found": "No event types found", + "average_event_duration": "Average Event Duration", + "most_booked_members": "Most Booked Members", + "least_booked_members": "Least Booked Members", + "events_created": "Events Created", + "events_completed": "Events Completed", + "events_cancelled": "Events Canceled", + "events_rescheduled": "Events Rescheduled", + "from_last_period": "from last period", + "from_to_date_period": "From: {{startDate}} To: {{endDate}}", + "redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.", + "event_trends": "Event Trends", + "clear_filters": "Clear Filters", + "clear": "Clear", + "hold": "Hold", + "on_booking_option": "Collect payment on booking", + "hold_option": "Charge no-show fee", + "card_held": "Card held", + "charge_card": "Charge card", + "card_charged": "Card charged", + "no_show_fee_amount": "{{amount, currency}} no-show fee", + "no_show_fee": "No-show Fee", + "submit_card": "Submit card", + "submit_payment_information": "Submit payment information", + "meeting_awaiting_payment_method": "Your meeting is awaiting a payment method", + "no_show_fee_charged_email_subject": "No-show fee of {{amount, currency}} charged for {{title}} at {{date}}", + "no_show_fee_charged_text_body": "No-show fee was charged", + "no_show_fee_charged_subtitle": "No-show fee of {{amount, currency}} was charged for the following event", + "error_charging_card": "Something went wrong charging the no-show fee. Please try again later.", + "collect_no_show_fee": "Collect no-show fee", + "no_show_fee_charged": "No-show fee charged", + "insights": "Insights", + "testing_workflow_info_message": "When testing this workflow, be aware that Emails and SMS can only be scheduled at least 1 hour in advance", + "insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.", + "acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.", + "card_details": "Card details", + "something_went_wrong_on_our_end":"Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right away for you.", + "please_provide_following_text_to_suppport":"Please provide the following text when contacting support to better help you", + "seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee", + "complete_your_booking": "Complete your booking", + "complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}", + "confirm_your_details": "Confirm your details", + "copy_invite_link": "Copy invite link", + "edit_invite_link": "Edit link settings", + "invite_link_copied": "Invite link copied", + "invite_link_deleted": "Invite link deleted", + "api_key_deleted":"API Key deleted", + "invite_link_updated": "Invite link settings saved", + "link_expires_after": "Links set to expire after...", + "one_day": "1 day", + "seven_days": "7 days", + "thirty_days": "30 days", + "three_months": "3 months", + "one_year": "1 year", + "team_invite_received": "You have been invited to join {{teamName}}", + "currency_string": "{{amount, currency}}", + "charge_card_dialog_body": "You are about to charge the attendee {{amount, currency}}. Are you sure you want to continue?", + "charge_attendee": "Charge attendee {{amount, currency}}", + "payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)", + "email_invite_team": "{{email}} has been invited", + "email_invite_team_bulk": "{{userCount}} users have been invited", + "error_collecting_card": "Error collecting card", + "image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit", + "unauthorized_workflow_error_message": "{{errorCode}}: You are not authorized to enable or disable this workflow", + "inline_embed": "Inline Embed", + "load_inline_content": "Loads your event type directly inline with your other website content.", + "floating_pop_up_button": "Floating pop-up button", + "floating_button_trigger_modal": "Puts a floating button on your site that triggers a modal with your event type.", + "pop_up_element_click": "Pop up via element click", + "open_dialog_with_element_click": "Open your calendar as a dialog when someone clicks an element.", + "need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.", + "book_my_cal": "Book my Cal", + "first_name": "First name", + "last_name": "Last name", + "first_last_name": "First name, Last name", + "invite_as": "Invite as", + "form_updated_successfully": "Form updated successfully.", + "disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees", + "disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.", + "disable_host_confirmation_emails": "Disable default confirmation emails for host", + "disable_host_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the host when the event is booked.", + "add_an_override": "Add an override", + "import_from_google_workspace": "Import users from Google Workspace", + "connect_google_workspace": "Connect Google Workspace", + "google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature", + "first_event_type_webhook_description": "Create your first webhook for this event type", + "install_app_on": "Install app on", + "create_for": "Create for", + "currency": "Currency", + "organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.", + "organization_banner_title": "Manage organizations with multiple teams", + "set_up_your_organization": "Set up your organization", + "organizations_description": "Organizations are shared environments where teams can create shared event types, apps, workflows and more.", + "must_enter_organization_name": "Must enter an organization name", + "must_enter_organization_admin_email": "Must enter your organization email address", + "admin_email": "Your organization email address", + "admin_username": "Administrator's username", + "organization_name": "Organization name", + "organization_url": "Organization URL", + "organization_verify_header": "Verify your organization email", + "organization_verify_email_body": "Please use the code below to verify your email address to continue setting up your organization.", + "additional_url_parameters": "Additional URL parameters", + "about_your_organization": "About your organization", + "about_your_organization_description": "Organizations are shared environments where you can create multiple teams with shared members, event types, apps, workflows and more.", + "create_your_teams": "Create your teams", + "create_your_teams_description": "Start scheduling together by adding your team members to your organization", + "invite_organization_admins": "Invite your organization admins", + "invite_organization_admins_description": "These admins will have access to all teams in your organization. You can add team admins and members later.", + "set_a_password": "Set a password", + "set_a_password_description": "This will create a new user account with your organization email and this password.", + "organization_logo": "Organization Logo", + "organization_about_description": "A few sentences about your organization. This will appear on your organization public profile page.", + "ill_do_this_later": "I'll do this later", + "verify_your_email": "Verify your email", + "enter_digit_code": "Enter the 6 digit code we sent to {{email}}", + "verify_email_organization": "Verify your email to create an organization", + "code_provided_invalid": "The code provided is not valid, try again", + "email_already_used": "Email already being used", + "organization_admin_invited_heading":"You've been invited to join {{orgName}}", + "organization_admin_invited_body":"Join your team at {{orgName}} and start focusing on meeting, not making meetings!", + "duplicated_slugs_warning": "The following teams couldn't be created due to duplicated slugs: {{slugs}}", + "team_names_empty": "Team names can't be empty", + "team_names_repeated": "Team names can't be repeated", + "user_belongs_organization": "User belongs to an organization", + "org_no_teams_yet": "This organization has no teams yet", + "org_no_teams_yet_description": "If you are an administrator, be sure to create teams to be shown here.", + "set_up": "Set up", + "my_profile": "My Profile", + "my_settings": "My Settings", + "crm": "CRM", + "messaging": "Messaging", + "sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)", + "org_admins_can_create_new_teams": "Only the admin of your organization can create new teams", + "google_new_spam_policy": "Google’s new spam policy could prevent you from receiving any email and calendar notifications about this booking.", + "resolve": "Resolve", + "no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.", + "org_name": "Organization name", + "org_url": "Organization URL", + "copy_link_org": "Copy link to organization", + "404_the_org": "The organization", + "404_the_team": "The team", + "404_claim_entity_org": "Claim your subdomain for your organization", + "404_claim_entity_team": "Claim this team and start managing schedules collectively", + "insights_all_org_filter": "All", + "insights_team_filter": "Team: {{teamName}}", + "insights_user_filter": "User: {{userName}}", + "insights_subtitle": "View booking insights across your events", + "location_options": "{{locationCount}} location options", + "custom_plan": "Custom Plan", + "email_embed": "Email Embed", + "add_times_to_your_email": "Select a few available times and embed them in your Email", + "select_time": "Select Time", + "select_date": "Select Date", + "see_all_available_times": "See all available times", + "org_team_names_example": "e.g. Marketing Team", + "org_team_names_example_1": "e.g. Marketing Team", + "org_team_names_example_2": "e.g. Sales Team", + "org_team_names_example_3": "e.g. Design Team", + "org_team_names_example_4": "e.g. Engineering Team", + "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.", + "organizations": "Organizations", + "org_admin_other_teams": "Other teams", + "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", + "attendee_last_name_variable": "Attendee last name", + "attendee_first_name_info": "The person booking's first name", + "attendee_last_name_info": "The person booking's last name", + "your_monthly_digest": "Your Monthly Digest", + "member_name": "Member Name", + "most_popular_events": "Most Popular Events", + "summary_of_events_for_your_team_for_the_last_30_days": "Here's your summary of popular events for your team {{teamName}} for the last 30 days", + "me": "Me", + "monthly_digest_email":"Monthly Digest Email", + "monthly_digest_email_for_teams": "Monthly digest email for teams", + "verify_team_tooltip": "Verify your team to enable sending messages to attendees", + "member_removed": "Member removed", + "my_availability": "My Availability", + "team_availability": "Team Availability", + "backup_code": "Backup Code", + "backup_codes": "Backup Codes", + "backup_code_instructions": "Each backup code can be used exactly once to grant access without your authenticator.", + "backup_codes_copied": "Backup codes copied!", + "incorrect_backup_code": "Backup code is incorrect.", + "lost_access": "Lost access", + "missing_backup_codes": "No backup codes found. Please generate them in your settings.", + "admin_org_notification_email_subject": "New organization created: pending action", + "hi_admin": "Hi Administrator", + "admin_org_notification_email_title": "An organization requires DNS setup", + "admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.

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.

Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.

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", + "team_no_event_types": "This team has no event types", + "seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations", + "include_calendar_event": "Include calendar event", + "oAuth": "OAuth", + "recently_added":"Recently added", + "no_members_found": "No members found", + "event_setup_length_error":"Event Setup: The duration must be at least 1 minute.", + "availability_schedules":"Availability Schedules", + "unauthorized":"Unauthorized", + "access_cal_account": "{{clientName}} would like access to your {{appName}} account", + "select_account_team": "Select account or team", + "allow_client_to": "This will allow {{clientName}} to", + "associate_with_cal_account":"Associate you with your personal info from {{clientName}}", + "see_personal_info":"See your personal info, including any personal info you've made publicly available", + "see_primary_email_address":"See your primary email address", + "connect_installed_apps":"Connect to your installed apps", + "access_event_type": "Read, edit, delete your event-types", + "access_availability": "Read, edit, delete your availability", + "access_bookings": "Read, edit, delete your bookings", + "allow_client_to_do": "Allow {{clientName}} to do this?", + "oauth_access_information": "By clicking allow, you allow this app to use your information in accordance with their terms of service and privacy policy. You can remove access in the {{appName}} App Store.", + "allow": "Allow", + "view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.", + "view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.", + "you_can_override_calendar_in_advanced_tab":"You can override this on a per-event basis in Advanced settings in each event type.", + "edit_users_availability":"Edit user's availability: {{username}}", + "resend_invitation": "Resend invitation", + "invitation_resent": "The invitation was resent.", + "add_client": "Add client", + "copy_client_secret_info": "After copying the secret you won't be able to view it anymore", + "add_new_client": "Add new Client", + "this_app_is_not_setup_already": "This app has not been setup yet", + "as_csv": "as CSV", + "overlay_my_calendar":"Overlay my calendar", + "overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.", + "view_overlay_calendar_events":"View your calendar events to prevent clashed booking.", + "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" +} diff --git a/apps/web/public/static/locales/km/vital.json b/apps/web/public/static/locales/km/vital.json new file mode 100644 index 0000000000..cdcadc6d4d --- /dev/null +++ b/apps/web/public/static/locales/km/vital.json @@ -0,0 +1,13 @@ +{ + "connected_vital_app": "ភ្ជាប់ជាមួយ", + "vital_app_sleep_automation": "Sleeping reschedule automation", + "vital_app_automation_description": "You can select different parameters to trigger the reschedule based on your sleeping metrics.", + "vital_app_parameter": "Parameter", + "vital_app_trigger": "Trigger at below or equal than", + "vital_app_save_button": "Save configuration", + "vital_app_total_label": "Total (total = rem + light sleep + deep sleep)", + "vital_app_duration_label": "Duration (duration = bedtime end - bedtime start)", + "vital_app_hours": "hours", + "vital_app_save_success": "Success saving your Vital Configurations", + "vital_app_save_error": "An error ocurred saving your Vital Configurations" +} diff --git a/packages/config/next-i18next.config.js b/packages/config/next-i18next.config.js index 4bbcbbc082..ad617bbb82 100644 --- a/packages/config/next-i18next.config.js +++ b/packages/config/next-i18next.config.js @@ -31,6 +31,7 @@ const config = { "vi", "zh-CN", "zh-TW", + "km", ], }, fallbackLng: { From 0014ca6865c9f7baa3cf838abd42b08fff9bc843 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 23 Oct 2023 12:06:24 +0000 Subject: [PATCH 031/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/km/common.json | 1844 +---------------- 1 file changed, 1 insertion(+), 1843 deletions(-) diff --git a/apps/web/public/static/locales/km/common.json b/apps/web/public/static/locales/km/common.json index ed535bcb13..01b355718a 100644 --- a/apps/web/public/static/locales/km/common.json +++ b/apps/web/public/static/locales/km/common.json @@ -44,7 +44,6 @@ "invite_team_member": "អញ្ជើញសមាជិកក្រុម", "invite_team_individual_segment": "អញ្ជើញជាបុគ្គល", "invite_team_bulk_segment": "ការនាំចូលច្រើន", - "invite_team_notifcation_badge": "Inv.", "your_event_has_been_scheduled": "ព្រឹត្តិការណ៍របស់អ្នកត្រូវបានកំណត់ពេល", "your_event_has_been_scheduled_recurring": "ព្រឹត្តិការណ៍កើតឡើងដដែលៗរបស់អ្នកត្រូវបានកំណត់ពេល", "accept_our_license": "ទទួលយកអាជ្ញាប័ណ្ណរបស់យើងដោយការផ្លាស់ប្តូរ .env អថេរ <1>NEXT_PUBLIC_LICENSE_CONSENT ទៅ '{{agree}}'.", @@ -193,318 +192,8 @@ "unconfirmed": "មិន​បាន​បញ្ជាក់", "guests": "ភ្ញៀវ", "guest": "ភ្ញៀវ", - "web_conferencing_details_to_follow": "Web conferencing details to follow in the confirmation email.", - "404_the_user": "The username", - "username": "Username", - "is_still_available": "is still available.", - "documentation": "Documentation", - "documentation_description": "Learn how to integrate our tools with your app", - "api_reference": "API Reference", - "api_reference_description": "A complete API reference for our libraries", - "blog": "Blog", - "blog_description": "Read our latest news and articles", - "join_our_community": "Join our community", - "join_our_discord": "Join our Discord", - "404_claim_entity_user": "Claim your username and schedule events", - "popular_pages": "Popular pages", - "register_now": "Register now", - "register": "Register", - "page_doesnt_exist": "This page does not exist.", - "check_spelling_mistakes_or_go_back": "Check for spelling mistakes or go back to the previous page.", - "404_page_not_found": "404: This page could not be found.", - "booker_event_not_found": "We could not find the event you are trying to book.", - "getting_started": "Getting Started", - "15min_meeting": "15 Min Meeting", - "30min_meeting": "30 Min Meeting", - "secret": "Secret", - "leave_blank_to_remove_secret": "Leave blank to remove secret", - "webhook_secret_key_description": "Ensure your server is only receiving the expected {{appName}} requests for security reasons", - "secret_meeting": "Secret Meeting", - "login_instead": "Login instead", - "already_have_an_account": "Already have an account?", - "create_account": "Create Account", - "confirm_password": "Confirm password", - "confirm_auth_change": "This will change the way you log in", - "confirm_auth_email_change": "Changing the email address will disconnect your current authentication method to log in to Cal.com. We will ask you to verify your new email address. Moving forward, you will be logged out and use your new email address to log in instead of your current authentication method after setting your password by following the instructions that will be sent to your mail.", - "reset_your_password": "Set your new password with the instructions sent to your email address.", - "email_change": "Log back in with your new email address and password.", - "create_your_account": "Create your account", - "sign_up": "Sign up", - "youve_been_logged_out": "You've been logged out", - "hope_to_see_you_soon": "We hope to see you again soon!", - "logged_out": "Logged out", - "please_try_again_and_contact_us": "Please try again and contact us if the issue persists.", - "incorrect_2fa_code": "Two-factor code is incorrect.", - "no_account_exists": "No account exists matching that email address.", - "2fa_enabled_instructions": "Two-factor authentication enabled. Please enter the six-digit code from your authenticator app.", - "2fa_enter_six_digit_code": "Enter the six-digit code from your authenticator app below.", - "create_an_account": "Create an account", - "dont_have_an_account": "Don't have an account?", - "2fa_code": "Two-Factor Code", - "sign_in_account": "Sign in to your account", - "sign_in": "Sign in", - "go_back_login": "Go back to the login page", - "error_during_login": "An error occurred when logging you in. Head back to the login screen and try again.", - "request_password_reset": "Send reset email", - "send_invite": "Send invite", - "forgot_password": "Forgot Password?", - "forgot": "Forgot?", - "done": "Done", - "all_done": "All done!", - "all": "All", - "yours": "Your account", - "available_apps": "Available Apps", - "available_apps_lower_case": "Available apps", - "available_apps_desc": "View popular apps below and explore more in our <1>App Store", - "fixed_host_helper": "Add anyone who needs to attend the event. <1>Learn more", - "round_robin_helper":"People in the group take turns and only one person will show up for the event.", - "check_email_reset_password": "Check your email. We sent you a link to reset your password.", - "finish": "Finish", - "organization_general_description": "Manage settings for your team language and timezone", - "few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.", - "nearly_there": "Nearly there!", - "nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.", - "set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.", - "set_availability": "Set your availability", - "availability_settings": "Availability Settings", - "continue_without_calendar": "Continue without calendar", - "continue_with": "Continue with {{appName}}", - "connect_your_calendar": "Connect your calendar", - "connect_your_video_app": "Connect your video apps", - "connect_your_video_app_instructions": "Connect your video apps to use them on your event types.", - "connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.", - "set_up_later": "Set up later", - "current_time": "Current time", - "details": "Details", - "welcome": "Welcome", - "welcome_back": "Welcome back", - "welcome_to_calcom": "Welcome to {{appName}}", - "welcome_instructions": "Tell us what to call you and let us know what timezone you’re in. You’ll be able to edit this later.", - "connect_caldav": "Connect to CalDav (Beta)", - "connect": "Connect", - "try_for_free": "Try it for free", - "create_booking_link_with_calcom": "Create your own booking link with {{appName}}", - "who": "Who", - "what": "What", - "when": "When", - "where": "Where", - "add_to_calendar": "Add to calendar", - "add_to_calendar_description":"Select where to add events when you’re booked.", - "add_events_to":"Add events to", - "add_another_calendar": "Add another calendar", - "other": "Other", - "email_sign_in_subject": "Your sign-in link for {{appName}}", - "emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.", - "emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.", - "emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.", - "needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.", - "needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.", - "user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.", - "user_needs_to_confirm_or_reject_booking_recurring": "{{user}} still needs to confirm or reject each booking of the recurring meeting.", - "meeting_is_scheduled": "This meeting is scheduled", - "meeting_is_scheduled_recurring": "The recurring events are scheduled", - "booking_submitted": "Your booking has been submitted", - "booking_submitted_recurring": "Your recurring meeting has been submitted", - "booking_confirmed": "Your booking has been confirmed", - "booking_confirmed_recurring": "Your recurring meeting has been confirmed", - "warning_recurring_event_payment": "Payments are not supported with Recurring Events yet", - "warning_payment_recurring_event": "Recurring events are not supported with Payments yet", - "enter_new_password": "Enter the new password you'd like for your account.", - "reset_password": "Reset Password", - "change_your_password": "Change your password", - "show_password": "Show password", - "hide_password": "Hide password", - "try_again": "Try Again", - "request_is_expired": "That Request is Expired.", - "reset_instructions": "Enter the email address associated with your account and we will send you a link to reset your password.", - "request_is_expired_instructions": "That request is expired. Go back and enter the email associated with your account and we will send you another link to reset your password.", - "whoops": "Whoops", - "login": "Login", - "success": "Success", - "failed": "Failed", - "password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.", - "layout": "Layout", - "bookerlayout_default_title": "Default view", - "bookerlayout_description": "You can select multiple and your bookers can switch views.", - "bookerlayout_user_settings_title": "Booking layout", - "bookerlayout_user_settings_description": "You can select multiple and bookers can switch views. This can be overridden on a per event basis.", - "bookerlayout_month_view": "Month", - "bookerlayout_week_view": "Weekly", - "bookerlayout_column_view": "Column", - "bookerlayout_error_min_one_enabled": "At least one layout has to be enabled.", - "bookerlayout_error_default_not_enabled": "The layout you selected as the default view is not part of the enabled layouts.", - "bookerlayout_error_unknown_layout": "The layout you selected is not a valid layout.", - "bookerlayout_override_global_settings": "You can manage this for all your event types in Settings -> <2>Appearance or <6>Override for this event only.", - "unexpected_error_try_again": "An unexpected error occurred. Try again.", - "sunday_time_error": "Invalid time on Sunday", - "monday_time_error": "Invalid time on Monday", - "tuesday_time_error": "Invalid time on Tuesday", - "wednesday_time_error": "Invalid time on Wednesday", - "thursday_time_error": "Invalid time on Thursday", - "friday_time_error": "Invalid time on Friday", - "saturday_time_error": "Invalid time on Saturday", - "error_end_time_before_start_time": "End time cannot be before start time", - "error_end_time_next_day": "End time cannot be greater than 24 hours", - "back_to_bookings": "Back to bookings", - "free_to_pick_another_event_type": "Feel free to pick another event anytime.", - "cancelled": "Canceled", - "cancellation_successful": "Cancellation successful", - "really_cancel_booking": "Really cancel your booking?", - "cannot_cancel_booking": "You cannot cancel this booking", - "reschedule_instead": "Instead, you could also reschedule it.", - "event_is_in_the_past": "The event is in the past", - "cancelling_event_recurring": "The event is one instance of a recurring event.", - "cancelling_all_recurring": "These are all remaining instances in the recurring event.", - "error_with_status_code_occured": "An error with status code {{status}} occurred.", - "error_event_type_url_duplicate": "An event type with this URL already exists.", - "error_event_type_unauthorized_create": "You are not able to create this event", - "error_event_type_unauthorized_update": "You are not able to edit this event", - "error_workflow_unauthorized_create": "You are not able to create this workflow", - "error_schedule_unauthorized_create": "You are not able to create this schedule", - "booking_already_cancelled": "This booking was already canceled", - "booking_already_accepted_rejected": "This booking was already accepted or rejected", - "go_back_home": "Go back home", - "or_go_back_home": "Or go back home", - "no_meeting_found": "No Meeting Found", - "no_meeting_found_description": "This meeting does not exist. Contact the meeting owner for an updated link.", - "no_status_bookings_yet": "No {{status}} bookings", - "no_status_bookings_yet_description": "You have no {{status}} bookings. {{description}}", - "event_between_users": "{{eventName}} between {{host}} and {{attendeeName}}", - "bookings": "Bookings", - "booking_not_found": "Booking not found", - "bookings_description": "See upcoming and past events booked through your event type links.", - "upcoming_bookings": "As soon as someone books a time with you it will show up here.", - "recurring_bookings": "As soon as someone books a recurring meeting with you it will show up here.", - "past_bookings": "Your past bookings will show up here.", - "cancelled_bookings": "Your canceled bookings will show up here.", - "unconfirmed_bookings": "Your unconfirmed bookings will show up here.", - "unconfirmed_bookings_tooltip": "Unconfirmed bookings", - "on": "on", - "and": "and", - "calendar_shows_busy_between": "Your calendar shows you as busy between", - "troubleshoot": "Troubleshoot", - "troubleshoot_description": "Understand why certain times are available and others are blocked.", - "overview_of_day": "Here is an overview of your day on", - "hover_over_bold_times_tip": "Tip: Hover over the bold times for a full timestamp", - "start_time": "Start time", - "end_time": "End time", - "buffer_time": "Buffer time", - "before_event": "Before event", - "after_event": "After event", - "event_buffer_default": "No buffer time", - "buffer": "Buffer", - "your_day_starts_at": "Your day starts at", - "your_day_ends_at": "Your day ends at", - "launch_troubleshooter": "Launch troubleshooter", - "troubleshoot_availability": "Troubleshoot your availability to explore why your times are showing as they are.", - "change_available_times": "Change available times", - "change_your_available_times": "Change your available times", - "change_start_end": "Change the start and end times of your day", - "change_start_end_buffer": "Set the start and end time of your day and a minimum buffer between your meetings.", - "current_start_date": "Currently, your day is set to start at", - "start_end_changed_successfully": "The start and end times for your day have been changed successfully.", - "and_end_at": "and end at", - "light": "Light", - "dark": "Dark", - "automatically_adjust_theme": "Automatically adjust theme based on invitee preferences", - "user_dynamic_booking_disabled": "Some of the users in the group have currently disabled dynamic group bookings", - "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_placeholder": "jdoe@example.com", "full_name": "ឈ្មោះ​ពេញ", - "browse_api_documentation": "Browse our API documentation", - "leverage_our_api": "Leverage our API for full control and customizability.", - "create_webhook": "Create Webhook", - "booking_cancelled": "Booking Canceled", - "booking_rescheduled": "Booking Rescheduled", - "recording_ready": "Recording Download Link Ready", - "booking_created": "Booking Created", - "booking_rejected": "Booking Rejected", - "booking_requested": "Booking Requested", - "booking_payment_initiated": "Booking Payment Initiated", - "meeting_ended": "Meeting Ended", - "form_submitted": "Form Submitted", - "booking_paid": "Booking Paid", - "event_triggers": "Event Triggers", - "subscriber_url": "Subscriber URL", - "create_new_webhook": "Create a new webhook", - "webhooks": "Webhooks", - "team_webhooks": "Team Webhooks", - "create_new_webhook_to_account": "Create a new webhook to your account", - "new_webhook": "New Webhook", - "receive_cal_meeting_data": "Receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.", - "receive_cal_event_meeting_data": "Receive {{appName}} meeting data at a specified URL, in real-time, when this event is scheduled or canceled.", - "responsive_fullscreen_iframe": "Responsive full screen iframe", - "loading": "Loading...", - "deleting": "Deleting...", - "standard_iframe": "Standard iframe", - "developer": "Developer", - "manage_developer_settings": "Manage your developer settings.", - "iframe_embed": "iframe Embed", - "embed_calcom": "The easiest way to embed {{appName}} on your website.", - "integrate_using_embed_or_webhooks": "Integrate with your website using our embed options, or get real-time booking information using custom webhooks.", - "schedule_a_meeting": "Schedule a meeting", - "view_and_manage_billing_details": "View and manage your billing details", - "view_and_edit_billing_details": "View and edit your billing details, as well as cancel your subscription.", - "go_to_billing_portal": "Go to the billing portal", - "need_anything_else": "Need anything else?", - "further_billing_help": "If you need any further help with billing, our support team are here to help.", - "contact": "Contact", - "our_support_team": "our support team", - "contact_our_support_team": "Contact our support team", - "uh_oh": "Uh oh!", - "no_event_types_have_been_setup": "This user hasn't set up any event types yet.", - "edit_logo": "Edit logo", - "upload_a_logo": "Upload a logo", - "upload_logo": "Upload logo", - "remove_logo": "Remove logo", - "enable": "Enable", - "code": "Code", - "code_is_incorrect": "Code is incorrect.", - "add_time_availability": "Add new time slot", - "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", - "2fa": "Two-Factor Authentication", - "2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication", - "enable_2fa": "Enable two-factor authentication", - "disable_2fa": "Disable two-factor authentication", - "disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", - "error_disabling_2fa": "Error disabling two-factor authentication", - "error_enabling_2fa": "Error setting up two-factor authentication", - "security": "Security", - "manage_account_security": "Manage your account's security.", - "password": "Password", - "password_updated_successfully": "Password updated successfully", - "password_has_been_changed": "Your password has been successfully changed.", - "error_changing_password": "Error changing password", - "session_timeout_changed": "Your session configuration has been updated successfully.", - "session_timeout_change_error": "Error updating session configuration", - "something_went_wrong": "Something went wrong.", - "something_doesnt_look_right": "Something doesn't look right?", - "please_try_again": "Please try again.", - "super_secure_new_password": "Your super secure new password", - "new_password": "New Password", - "your_old_password": "Your old password", - "current_password": "Current Password", - "change_password": "Change Password", - "change_secret": "Change Secret", - "new_password_matches_old_password": "New password matches your old password. Please choose a different password.", - "forgotten_secret_description": "If you have lost or forgotten this secret, you can change it, but be aware that all integrations using this secret will need to be updated", - "current_incorrect_password": "Current password is incorrect", - "password_hint_caplow": "Mix of uppercase & lowercase letters", - "password_hint_min": "Minimum 7 characters long", - "password_hint_admin_min": "Minimum 15 characters long", - "password_hint_num": "Contain at least 1 number", - "max_limit_allowed_hint": "Must be {{limit}} or fewer characters long", - "invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters", - "incorrect_password": "Password is incorrect.", - "incorrect_email_password": "Email or password is incorrect.", - "use_setting": "Use setting", - "am_pm": "am/pm", - "time_options": "Time options", "january": "មករា", "february": "កុម្ភៈ", "march": "មីនា", @@ -524,164 +213,12 @@ "friday": "សុក្រ", "saturday": "សៅរ៍", "sunday": "អាទិត្យ", - "all_booked_today": "All booked.", - "slots_load_fail": "Could not load the available time slots.", - "additional_guests": "Add guests", - "your_name": "Your name", - "your_full_name": "Your full name", - "no_name": "No name", - "enter_number_between_range": "Please enter a number between 1 and {{maxOccurences}}", - "email_address": "Email address", - "enter_valid_email": "Please enter a valid email", - "location": "Location", - "address": "Address", - "enter_address": "Enter address", - "in_person_attendee_address": "In Person (Attendee Address)", - "yes": "yes", - "no": "no", - "additional_notes": "Additional notes", - "booking_fail": "Could not book the meeting.", - "reschedule_fail": "Could not reschedule the meeting.", - "share_additional_notes": "Please share anything that will help prepare for our meeting.", - "booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}", - "booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}", - "in_person_meeting": "In-person meeting", - "in_person": "In Person (Organizer Address)", - "link_meeting": "Link meeting", - "phone_number": "Phone Number", - "attendee_phone_number": "Attendee Phone Number", - "organizer_phone_number": "Organizer Phone Number", - "enter_phone_number": "Enter phone number", - "reschedule": "Reschedule", - "reschedule_this": "Reschedule instead", - "book_a_team_member": "Book a team member instead", - "or": "OR", - "go_back": "Go back", - "email_or_username": "Email or Username", - "send_invite_email": "Send an invite email", - "role": "Role", - "edit_role": "Edit Role", - "edit_team": "Edit team", - "reject": "Reject", - "reject_all": "Reject all", - "accept": "Accept", - "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", - "your_team_updated_successfully": "Your team has been updated successfully.", - "your_org_updated_successfully": "Your Org has been updated successfully.", - "about": "About", - "team_description": "A few sentences about your team. This will appear on your team's url page.", - "org_description": "A few sentences about your organization. This will appear on your organization's url page.", - "members": "Members", - "organization_members": "Organization members", - "member": "Member", - "number_member_one": "{{count}} member", - "number_member_other": "{{count}} members", - "number_selected": "{{count}} selected", - "owner": "Owner", - "admin": "Admin", - "administrator_user": "Administrator user", - "lets_create_first_administrator_user": "Let's create the first administrator user.", - "admin_user_created": "Administrator user setup", - "admin_user_created_description": "You have already created an administrator user. You can now log in to your account.", - "new_member": "New Member", - "invite": "Invite", - "add_team_members": "Add team members", - "add_team_members_description": "Invite others to join your team", - "add_team_member": "Add team member", - "invite_new_member": "Invite a new team member", - "invite_new_member_description": "Note: This will <1>cost an extra seat ($15/m) on your subscription.", - "invite_new_team_member": "Invite someone to your team.", - "upload_csv_file": "Upload a .csv file", - "invite_via_email": "Invite via email", - "change_member_role": "Change team member role", - "disable_cal_branding": "Disable {{appName}} branding", - "disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.", - "hide_book_a_team_member": "Hide Book a Team Member Button", - "hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.", - "danger_zone": "Danger zone", - "account_deletion_cannot_be_undone":"Careful. Account deletion cannot be undone.", - "back": "Back", - "cancel": "Cancel", - "cancel_all_remaining": "Cancel all remaining", - "apply": "Apply", - "cancel_event": "Cancel event", - "continue": "Continue", - "confirm": "Confirm", - "confirm_all": "Confirm all", - "disband_team": "Disband Team", - "disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you've shared this team link with will no longer be able to book using it.", - "disband_org": "Disband Organization", - "disband_org_confirmation_message": "Are you sure you want to disband this Org? All teams and members will be deleted.", - "remove_member_confirmation_message": "Are you sure you want to remove this member from the team?", - "confirm_disband_team": "Yes, disband team", - "confirm_remove_member": "Yes, remove member", - "remove_member": "Remove member", - "manage_your_team": "Manage your team", - "no_teams": "You don't have any teams yet.", - "no_teams_description": "Teams allow others to book events shared between your coworkers.", "submit": "ដាក់ស្នើ", "delete": "លុប", "update": "ធ្វើបច្ចុប្បន្នភាព", "save": "រក្សាទុក", "pending": "កំពុងរង់ចាំ", - "open_options": "Open options", - "copy_link": "Copy link to event", "share": "ចែករំលែក", - "share_event": "Would you mind booking my cal or send me your link?", - "copy_link_team": "Copy link to team", - "leave_team": "Leave team", - "confirm_leave_team": "Yes, leave team", - "leave_team_confirmation_message": "Are you sure you want to leave this team? You will no longer be able to book using it.", - "user_from_team": "{{user}} from {{team}}", - "preview": "Preview", - "link_copied": "Link copied!", - "private_link_copied": "Private link copied!", - "link_shared": "Link shared!", - "title": "Title", - "description": "Description", - "apps_status": "Apps Status", - "quick_video_meeting": "A quick video meeting.", - "scheduling_type": "Scheduling Type", - "preview_team": "Preview team", - "collective": "Collective", - "collective_description": "Schedule meetings when all selected team members are available.", - "duration": "Duration", - "available_durations": "Available durations", - "default_duration": "Default duration", - "default_duration_no_options": "Please choose available durations first", - "multiple_duration_mins": "{{count}} $t(minute_timeUnit)", - "minutes": "Minutes", - "round_robin": "Round Robin", - "round_robin_description": "Cycle meetings between multiple team members.", - "managed_event": "Managed Event", - "username_placeholder": "username", - "managed_event_description": "Create & distribute event types in bulk to team members", - "managed": "Managed", - "managed_event_url_clarification": "\"username\" will be filled by the username of the members assigned", - "assign_to": "Assign to", - "add_members": "Add members...", - "count_members_one": "{{count}} member", - "count_members_other": "{{count}} members", - "no_assigned_members": "No assigned members", - "assigned_to": "Assigned to", - "start_assigning_members_above": "Start assigning members above", - "locked_fields_admin_description": "Members will not be able to edit this", - "locked_fields_member_description": "This option was locked by the team admin", - "url": "URL", - "hidden": "Hidden", - "readonly": "Readonly", - "one_time_link": "One-time link", - "plan_description": "You're currently on the {{plan}} plan.", - "plan_upgrade_invitation": "Upgrade your account to the PRO plan to unlock all of the features we have to offer.", - "plan_upgrade": "You need to upgrade your plan to have more than one active event type.", - "plan_upgrade_teams": "You need to upgrade your plan to create a team.", - "plan_upgrade_instructions": "You can <1>upgrade here.", "event_types_page_title": "ប្រភេទព្រឹត្តិការណ៍", "event_types_page_subtitle": "បង្កើតព្រឹត្តិការណ៍ដើម្បីចែករំលែកសម្រាប់មនុស្សដើម្បីកក់នៅលើប្រតិទិនរបស់អ្នក។", "new": "ថ្មី", @@ -692,7 +229,6 @@ "event_type_updated_successfully": "ប្រភេទព្រឹត្តិការណ៍ {{eventTypeTitle}} បានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ", "event_type_deleted_successfully": "ប្រភេទព្រឹត្តិការណ៍ត្រូវបានលុបដោយជោគជ័យ", "hours": "ម៉ោង", - "people": "People", "your_email": "អ៊ីមែល​របស់​អ្នក", "change_avatar": "ផ្លាស់ប្តូរ Avatar", "upload_avatar": "បង្ហោះ Avatar", @@ -701,1397 +237,19 @@ "first_day_of_week": "ថ្ងៃដំបូងនៃសប្តាហ៍", "repeats_up_to_one": "ធ្វើម្តងទៀតរហូតដល់ {{count}} ដង", "repeats_up_to_other": "ធ្វើម្តងទៀតរហូតដល់ {{count}} ដង", - "every_for_freq": "Every {{freq}} for", "event_remaining_one": "{{count}} ព្រឹត្តិការណ៍ដែលនៅសល់", "event_remaining_other": "{{count}} ព្រឹត្តិការណ៍ដែលនៅសល់", "repeats_every": "ធ្វើម្តងទៀតរៀងរាល់", - "occurrence_one": "occurrence", - "occurrence_other": "occurrences", - "weekly_one": "week", - "weekly_other": "weeks", - "monthly_one": "month", - "monthly_other": "months", - "yearly_one": "year", - "yearly_other": "years", - "plus_more": "{{count}} more", - "max": "Max", - "single_theme": "Single Theme", - "brand_color": "Brand Color", - "light_brand_color": "Brand Color (Light Theme)", - "dark_brand_color": "Brand Color (Dark Theme)", - "file_not_named": "File is not named [idOrSlug]/[user]", - "create_team": "Create Team", - "name": "Name", - "create_new_team_description": "Create a new team to collaborate with users.", - "create_new_team": "Create a new team", - "open_invitations": "Open Invitations", - "new_team": "New Team", - "create_first_team_and_invite_others": "Create your first team and invite other users to work together.", - "create_team_to_get_started": "Create a team to get started", - "teams": "Teams", - "team": "Team", - "organization": "Organization", - "team_billing": "Team Billing", - "team_billing_description": "Manage billing for your team", - "upgrade_to_flexible_pro_title": "We've changed billing for teams", - "upgrade_to_flexible_pro_message": "There are members in your team without a seat. Upgrade your pro plan to cover missing seats.", - "changed_team_billing_info": "As of January 2022 we charge on a per-seat basis for team members. Members of your team who had PRO for free are now on a 14 day trial. Once their trial expires these members will be hidden from your team unless you upgrade now.", - "create_manage_teams_collaborative": "Create and manage teams to use collaborative features.", - "only_available_on_pro_plan": "This feature is only available in Pro plan", - "remove_cal_branding_description": "In order to remove the {{appName}} branding from your booking pages, you need to upgrade to a Pro account.", - "edit_profile_info_description": "Edit your profile information, which shows on your scheduling link.", - "change_email_tip": "You may need to log out and back in to see the change take effect.", - "little_something_about": "A little something about yourself.", - "profile_updated_successfully": "Profile updated successfully", - "your_user_profile_updated_successfully": "Your user profile has been updated successfully.", - "user_cannot_found_db": "User seems logged in but cannot be found in the db", - "embed_and_webhooks": "Embed & Webhooks", - "enabled": "Enabled", - "disabled": "Disabled", - "disable": "Disable", - "billing": "Billing", - "manage_your_billing_info": "Manage your billing information and cancel your subscription.", - "availability": "Availability", - "edit_availability": "Edit availability", - "configure_availability": "Configure times when you are available for bookings.", - "copy_times_to": "Copy times to", - "copy_times_to_tooltip": "Copy times to …", - "change_weekly_schedule": "Change your weekly schedule", - "logo": "Logo", - "error": "Error", - "at_least_characters_one": "Please enter at least one character", - "at_least_characters_other": "Please enter at least {{count}} characters", - "team_logo": "Team Logo", - "add_location": "Add a location", - "attendees": "Attendees", - "add_attendees": "Add attendees", - "show_advanced_settings": "Show advanced settings", - "event_name": "Event Name", - "event_name_in_calendar": "Event name in calendar", - "event_name_tooltip": "The name that will appear in calendars", - "meeting_with_user": "{Event type title} between {Organiser} & {Scheduler}", - "additional_inputs": "Additional Inputs", - "additional_input_description": "Require scheduler to input additional inputs prior the booking is confirmed", - "label": "Label", - "placeholder": "Placeholder", - "type": "Type", - "edit": "Edit", - "add_input": "Add an Input", - "disable_notes": "Hide notes in calendar", - "disable_notes_description": "For privacy reasons, additional inputs and notes will be hidden in the calendar entry. They will still be sent to your email.", - "requires_confirmation_description": "The booking needs to be manually confirmed before it is pushed to the integrations and a confirmation mail is sent.", - "recurring_event": "Recurring Event", - "recurring_event_description": "People can subscribe for recurring events", - "starting": "Starting", - "disable_guests": "Disable Guests", - "disable_guests_description": "Disable adding additional guests while booking.", - "private_link": "Generate private link", - "enable_private_url": "Enable Private URL", - "private_link_label": "Private link", - "private_link_hint": "Your private link will regenerate after each use", - "copy_private_link": "Copy private link", - "private_link_description": "Generate a private URL to share without exposing your {{appName}} username", - "invitees_can_schedule": "Invitees can schedule", - "date_range": "Date Range", - "calendar_days": "calendar days", - "business_days": "business days", - "set_address_place": "Set an address or place", - "set_link_meeting": "Set a link to the meeting", - "cal_invitee_phone_number_scheduling": "{{appName}} will ask your invitee to enter a phone number before scheduling.", - "cal_provide_google_meet_location": "{{appName}} will provide a Google Meet location.", - "cal_provide_zoom_meeting_url": "{{appName}} will provide a Zoom meeting URL.", - "cal_provide_tandem_meeting_url": "{{appName}} will provide a Tandem meeting URL.", - "cal_provide_video_meeting_url": "{{appName}} will provide a video meeting URL.", - "cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.", - "cal_provide_huddle01_meeting_url": "{{appName}} will provide a Huddle01 web3 video meeting URL.", - "cal_provide_teams_meeting_url": "{{appName}} will provide a MS Teams meeting URL. NOTE: MUST HAVE A WORK OR SCHOOL ACCOUNT", - "require_payment": "Require Payment", - "you_need_to_add_a_name": "You need to add a name", - "commission_per_transaction": "commission per transaction", - "event_type_updated_successfully_description": "Your event type has been updated successfully.", - "hide_event_type": "Hide event type", - "edit_location": "Edit location", - "into_the_future": "into the future", - "when_booked_with_less_than_notice": "When booked with less than notice", - "within_date_range": "Within a date range", - "indefinitely_into_future": "Indefinitely into the future", - "add_new_custom_input_field": "Add new custom input field", - "quick_chat": "Quick Chat", - "add_new_team_event_type": "Add a new team event type", - "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", - "minimum_booking_notice": "Minimum Notice", - "offset_toggle": "Offset start times", - "offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes", - "offset_start": "Offset by", - "offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}", - "slot_interval": "Time-slot intervals", - "slot_interval_default": "Use event length (default)", - "delete_event_type": "Delete event type?", - "delete_managed_event_type": "Delete managed event type?", - "delete_event_type_description": "Anyone who you've shared this link with will no longer be able to book using it.", - "delete_managed_event_type_description": "
  • Members assigned to this event type will also have their event types deleted.
  • Anyone who they've shared their link with will no longer be able to book using it.
", - "confirm_delete_event_type": "Yes, delete", - "delete_account": "Delete account", - "confirm_delete_account": "Yes, delete account", - "delete_account_confirmation_message": "Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.", - "integrations": "Integrations", - "apps": "Apps", - "apps_description": "Here you can find a list of your apps", - "apps_listing": "App listing", - "category_apps": "{{category}} apps", - "app_store": "App Store", - "app_store_description": "Connecting people, technology and the workplace.", - "settings": "Settings", - "event_type_moved_successfully": "Event type has been moved successfully", - "next_step_text": "Next Step", - "next_step": "Skip step", - "prev_step": "Prev step", - "install": "Install", - "installed": "Installed", - "active_install_one": "{{count}} active install", - "active_install_other": "{{count}} active installs", - "globally_install": "Globally installed", - "app_successfully_installed": "App successfully installed", - "app_could_not_be_installed": "App could not be installed", - "disconnect": "Disconnect", - "embed_your_calendar": "Embed your calendar within your webpage", - "connect_your_favourite_apps": "Connect your favourite apps.", - "automation": "Automation", - "configure_how_your_event_types_interact": "Configure how your event types should interact with your calendars.", - "toggle_calendars_conflict": "Toggle the calendars you want to check for conflicts to prevent double bookings.", - "connect_additional_calendar": "Connect additional calendar", - "calendar_updated_successfully": "Calendar updated successfully", - "conferencing": "Conferencing", - "calendar": "Calendar", - "payments": "Payments", - "not_installed": "Not installed", - "error_password_mismatch": "Passwords don't match.", - "error_required_field": "This field is required.", - "status": "Status", - "team_view_user_availability": "View user availability", - "team_view_user_availability_disabled": "User needs to accept invite to view availability", - "set_as_away": "Set yourself as away", - "set_as_free": "Disable away status", - "toggle_away_error": "Error updating away status", - "user_away": "This user is currently away.", - "user_away_description": "The person you are trying to book has set themselves to away, and therefore is not accepting new bookings.", - "meet_people_with_the_same_tokens": "Meet people with the same tokens", - "only_book_people_and_allow": "Only book and allow bookings from people who share the same tokens, DAOs, or NFTs.", - "account_created_with_identity_provider": "Your account was created using an Identity Provider.", - "account_managed_by_identity_provider": "Your account is managed by {{provider}}", - "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", - "access_token": "Access token", - "visit_roadmap": "Roadmap", - "featured_categories": "Featured Categories", - "popular_categories": "Popular Categories", - "number_apps_one": "{{count}} App", - "number_apps_other": "{{count}} Apps", - "trending_apps": "Trending Apps", - "most_popular": "Most Popular", - "installed_apps": "Installed Apps", - "free_to_use_apps": "Free", - "no_category_apps": "No {{category}} apps", - "all_apps": "All apps", - "no_category_apps_description_calendar": "Add a calendar app to check for conflicts to prevent double bookings", - "no_category_apps_description_conferencing": "Try adding a conference app for video calls with your clients", - "no_category_apps_description_payment": "Add a payment app to ease transaction between you and your clients", - "no_category_apps_description_analytics": "Add an analytics app for your booking pages", - "no_category_apps_description_automation": "Add an automation app to use", - "no_category_apps_description_other": "Add any other type of app to do all sorts of things", - "no_category_apps_description_web3": "Add a web3 app for your booking pages", - "no_category_apps_description_messaging": "Add a messaging app to set up custom notifications & reminders", - "no_category_apps_description_crm": "Add a CRM app to keep track of who you've met with", - "installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.", - "installed_app_payment_description": "Configure which payment processing services to use when charging your clients.", - "installed_app_analytics_description": "Configure which analytics apps to use for your booking pages", - "installed_app_other_description": "All your installed apps from other categories.", - "installed_app_conferencing_description": "Configure which conferencing apps to use", - "installed_app_automation_description": "Configure which automation apps to use", - "installed_app_web3_description": "Configure which web3 apps to use for your booking pages", - "installed_app_messaging_description": "Configure which messaging apps to use for setting up custom notifications & reminders", - "installed_app_crm_description": "Configure which CRM apps to use for keeping track of who you've met with", - "analytics": "Analytics", - "empty_installed_apps_headline": "No apps installed", - "empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.", - "empty_installed_apps_button": "Browse App Store", - "manage_your_connected_apps": "Manage your installed apps or change settings", - "browse_apps": "Browse Apps", - "features": "Features", - "permissions": "Permissions", - "terms_and_privacy": "Terms and Privacy", - "published_by": "Published by {{author}}", - "subscribe": "Subscribe", - "buy": "Buy", - "install_app": "Install App", - "categories": "Categories", - "pricing": "Pricing", - "learn_more": "Learn more", - "privacy_policy": "Privacy Policy", - "terms_of_service": "Terms of Service", - "remove": "Remove", - "add": "Add", - "installed_other": "{{count}} installed", - "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 <2>/auth/setup to enter a license key. If a license key is already in place, please contact <5>{{SUPPORT_MAIL_ADDRESS}} 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 to enter a license key.", - "missing_license": "Missing License", - "signup_requires": "Commercial license required", - "signup_requires_description": "{{companyName}} currently does not offer a free open source version of the sign up page. To receive full access to the signup components you need to acquire a commercial license. For personal use we recommend the Prisma Data Platform or any other Postgres interface to create accounts.", - "next_steps": "Next Steps", - "acquire_commercial_license": "Acquire a commercial license", - "the_infrastructure_plan": "The infrastructure plan is usage-based and has startup-friendly discounts.", - "prisma_studio_tip": "Create an account via Prisma Studio", - "prisma_studio_tip_description": "Learn how to set up your first user", - "contact_sales": "Contact Sales", - "error_404": "Error 404", - "default": "Default", - "set_to_default": "Set to Default", - "new_schedule_btn": "New schedule", - "add_new_schedule": "Add a new schedule", - "add_new_calendar": "Add a new calendar", - "set_calendar": "Set where to add new events to when you're booked.", - "delete_schedule": "Delete schedule", - "delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.", - "schedule_created_successfully": "{{scheduleName}} schedule created successfully", - "availability_updated_successfully": "{{scheduleName}} schedule updated successfully", - "schedule_deleted_successfully": "Schedule deleted successfully", - "default_schedule_name": "Working Hours", - "new_schedule_heading": "Create an availability schedule", - "new_schedule_description": "Creating availability schedules allows you to manage availability across event types. They can be applied to one or more event types.", - "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:", - "example_name": "John Doe", "time_format": "ទម្រង់ពេលវេលា", - "12_hour": "12 hour", - "24_hour": "24 hour", - "12_hour_short": "12h", - "24_hour_short": "24h", - "redirect_success_booking": "Redirect on booking ", - "you_are_being_redirected": "You are being redirected to {{ url }} in $t(second, {\"count\": {{seconds}} }).", - "external_redirect_url": "https://example.com/redirect-to-my-success-page", - "redirect_url_description": "Redirect to a custom URL after a successful booking", - "duplicate": "Duplicate", - "offer_seats": "Offer seats", - "offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.", - "seats_available_one": "Seat available", - "seats_available_other": "Seats available", - "seats_nearly_full": "Seats almost full", - "seats_half_full": "Seats filling fast", - "number_of_seats": "Number of seats per booking", - "enter_number_of_seats": "Enter number of seats", - "you_can_manage_your_schedules": "You can manage your schedules on the Availability page.", - "booking_full": "No more seats available", - "api_keys": "API keys", - "api_key": "API key", - "test_api_key": "Test API key", - "test_passed": "Test passed!", - "test_failed": "Test failed", - "provide_api_key": "Provide API key", - "api_key_modal_subtitle": "API keys allow you to make API calls for your own account.", - "api_keys_subtitle": "Generate API keys to use for accessing your own account.", - "create_api_key": "Create an API key", - "personal_note": "Name this key", - "personal_note_placeholder": "E.g. Development", - "api_key_no_note": "Nameless API key", - "api_key_never_expires": "This API key has no expiration date", - "edit_api_key": "Edit API key", - "success_api_key_created": "API key created successfully", - "success_api_key_edited": "API key updated successfully", - "create": "Create", - "success_api_key_created_bold_tagline": "Save this API key somewhere safe.", - "you_will_only_view_it_once": "You will not be able to view it again once you close this modal.", - "copy_to_clipboard": "Copy to clipboard", - "enabled_after_update": "Enabled after update", - "enabled_after_update_description": "The private link will work after saving", - "confirm_delete_api_key": "Revoke this API key", - "revoke_api_key": "Revoke API key", - "api_key_copied": "API key copied!", - "api_key_expires_on":"The API key will expire on", - "delete_api_key_confirm_title": "Permanently remove this API key from your account?", - "copy": "Copy", - "expire_date": "Expiration date", - "expired": "Expired", - "never_expires": "Never expires", - "expires": "Expires", - "request_reschedule_booking": "Request to reschedule your booking", - "reason_for_reschedule": "Reason for reschedule", - "book_a_new_time": "Book a new time", - "reschedule_request_sent": "Reschedule request sent", - "reschedule_modal_description": "This will cancel the scheduled meeting, notify the scheduler and ask them to pick a new time.", - "reason_for_reschedule_request": "Reason for reschedule request", - "send_reschedule_request": "Request reschedule ", - "edit_booking": "Edit booking", - "reschedule_booking": "Reschedule booking", - "former_time": "Former time", - "confirmation_page_gif": "Add a GIF to your confirmation page", - "search": "Search", - "impersonate": "Impersonate", - "user_impersonation_heading": "User Impersonation", - "user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.", - "team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.", - "make_team_private": "Make team private", - "make_team_private_description": "Your team members won't be able to see other team members when this is turned on.", - "you_cannot_see_team_members": "You cannot see all the team members of a private team.", - "allow_booker_to_select_duration": "Allow booker to select duration", - "impersonate_user_tip": "All uses of this feature is audited.", - "impersonating_user_warning": "Impersonating username \"{{user}}\".", - "impersonating_stop_instructions": "Click here to stop", - "event_location_changed": "Updated - Your event changed the location", - "location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}", - "current_location": "Current Location", - "new_location": "New Location", - "session": "Session", - "session_description": "Control your account session", - "session_timeout_after": "Timeout session after", - "session_timeout": "Session timeout", - "session_timeout_description": "Invalidate your session after a certain amount of time.", - "no_location": "No location defined", - "set_location": "Set Location", - "update_location": "Update Location", - "location_updated": "Location updated", - "email_validation_error": "That doesn't look like an email address", - "place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.", - "create_update_react_component": "Create or update an existing React component as shown below.", - "copy_code": "Copy Code", - "code_copied": "Code copied!", - "how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?", - "choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.", - "setting_up_zapier": "Setting up your Zapier integration", - "setting_up_make": "Setting up your Make integration", - "generate_api_key": "Generate API key", - "generate_api_key_description": "Generate an API key to use with {{appName}} at", - "your_unique_api_key": "Your unique API key", - "copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.", - "zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.<1>Select Cal.com as your Trigger app. Also choose a Trigger event.<2>Choose your account and then enter your Unique API Key.<3>Test your Trigger.<4>You're set!", - "make_setup_instructions": "<0>Go to <1><0>Make Invite Link and install the Cal.com app.<1>Log into your Make account and create a new Scenario.<2>Select Cal.com as your Trigger app. Also choose a Trigger event.<3>Choose your account and then enter your Unique API Key.<4>Test your Trigger.<5>You're set!", - "install_zapier_app": "Please first install the Zapier App in the app store.", - "install_make_app": "Please first install the Make App in the app store.", - "connect_apple_server": "Connect to Apple Server", - "calendar_url": "Calendar URL", - "apple_server_generate_password": "Generate an app specific password to use with {{appName}} at", - "credentials_stored_encrypted": "Your credentials will be stored and encrypted.", - "it_stored_encrypted": "It will be stored and encrypted.", - "go_to_app_store": "Go to App Store", - "calendar_error": "Try reconnecting your calendar with all necessary permissions", - "set_your_phone_number": "Set a phone number for the meeting", - "calendar_no_busy_slots": "There are no busy slots", - "display_location_label": "Display on booking page", - "display_location_info_badge": "Location will be visible before the booking is confirmed", - "add_gif": "Add GIF", - "search_giphy": "Search Giphy", - "add_link_from_giphy": "Add link from Giphy", - "add_gif_to_confirmation": "Adding a GIF to confirmation page", - "find_gif_spice_confirmation": "Find GIF to spice up your confirmation page", - "share_feedback": "Share feedback", - "resources": "Resources", - "support_documentation": "Support Documentation", - "developer_documentation": "Developer Documentation", - "get_in_touch": "Get in touch", - "contact_support": "Contact Support", - "feedback": "Feedback", - "submitted_feedback": "Thank you for your feedback!", - "feedback_error": "Error sending feedback", - "comments": "Share your comments here:", - "booking_details": "Booking details", - "or_lowercase": "or", - "nevermind": "Nevermind", - "go_to": "Go to: ", - "zapier_invite_link": "Zapier Invite Link", - "meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.", - "dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee", - "event_location": "Event's location", - "reschedule_optional": "Reason for rescheduling (optional)", - "reschedule_placeholder": "Let others know why you need to reschedule", - "event_cancelled": "This event is canceled", - "emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.", - "this_input_will_shown_booking_this_event": "This input will be shown when booking this event", - "meeting_url_in_confirmation_email": "Meeting url is in the confirmation email", - "url_start_with_https": "URL needs to start with http:// or https://", - "number_provided": "Phone number will be provided", - "before_event_trigger": "before event starts", - "event_cancelled_trigger": "when event is canceled", - "new_event_trigger": "when new event is booked", - "email_host_action": "send email to host", - "email_attendee_action": "send email to attendees", - "sms_attendee_action": "Send SMS to attendee", - "sms_number_action": "send SMS to a specific number", - "send_reminder_sms": "Easily send meeting reminders via SMS to your attendees", - "whatsapp_number_action": "send WhatsApp message to a specific number", - "whatsapp_attendee_action": "send WhatsApp message to attendee", - "workflows": "Workflows", - "new_workflow_btn": "New Workflow", - "add_new_workflow": "Add a new workflow", - "reschedule_event_trigger": "when event is rescheduled", - "trigger": "Trigger", - "triggers": "Triggers", - "action": "Action", - "workflows_to_automate_notifications": "Create workflows to automate notifications and reminders", - "workflow_name": "Workflow name", - "custom_workflow": "Custom workflow", - "workflow_created_successfully": "{{workflowName}} created successfully", - "delete_workflow_description": "Are you sure you want to delete this workflow?", - "delete_workflow": "Delete Workflow", - "confirm_delete_workflow": "Yes, delete workflow", - "workflow_deleted_successfully": "Workflow deleted successfully", - "how_long_before": "How long before event starts?", - "day_timeUnit": "days", - "hour_timeUnit": "hours", - "minute_timeUnit": "mins", - "new_workflow_heading": "Create your first workflow", - "new_workflow_description": "Workflows enable you to automate sending reminders and notifications.", - "active_on": "Active on", - "workflow_updated_successfully": "{{workflowName}} workflow updated successfully", - "premium_to_standard_username_description": "This is a standard username and updating will take you to billing to downgrade.", - "current": "Current", - "premium": "premium", - "standard": "standard", - "confirm_username_change_dialog_title": "Confirm username change", - "change_username_standard_to_premium": "As you are changing from a standard to a premium username, you will be taken to the checkout to upgrade.", - "change_username_premium_to_standard": "As you are changing from a premium to a standard username, you will be taken to the checkout to downgrade.", - "go_to_stripe_billing": "Go to billing", - "stripe_description": "Require payment for bookings (0.5% + €0.10 commission per transaction)", - "trial_expired": "Your trial has expired", - "remove_app": "Remove App", - "yes_remove_app": "Yes, remove app", - "are_you_sure_you_want_to_remove_this_app": "Are you sure you want to remove this app?", - "app_removed_successfully": "App removed successfully", - "error_removing_app": "Error removing app", - "web_conference": "Web conference", - "requires_confirmation": "Requires confirmation", - "always_requires_confirmation": "Always", - "requires_confirmation_threshold": "Requires confirmation if booked with < {{time}} $t({{unit}}_timeUnit) notice", - "may_require_confirmation": "May require confirmation", - "nr_event_type_one": "{{count}} event type", - "nr_event_type_other": "{{count}} event types", - "add_action": "Add action", - "set_whereby_link": "Set Whereby link", - "invalid_whereby_link": "Please enter a valid Whereby Link", - "set_around_link": "Set Around.Co link", - "invalid_around_link": "Please enter a valid Around Link", - "set_riverside_link": "Set Riverside link", - "invalid_riverside_link": "Please enter a valid Riverside Link", - "invalid_ping_link": "Please enter a valid Ping.gg Link", - "add_exchange2013": "Connect Exchange 2013 Server", - "add_exchange2016": "Connect Exchange 2016 Server", - "custom_template": "Custom template", - "email_body": "Email body", - "text_message": "Text message", - "specific_issue": "Have a specific issue?", - "browse_our_docs": "browse our docs", - "choose_template": "Choose a template", - "custom": "Custom", - "reminder": "Reminder", - "rescheduled": "Rescheduled", - "completed": "Completed", - "reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}", - "not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.", - "minute_one": "{{count}} minute", - "minute_other": "{{count}} minutes", - "hour_one": "{{count}} hour", - "hour_other": "{{count}} hours", - "invalid_input": "Invalid input", - "broken_video_action": "We could not add the <1>{{location}} meeting link to your scheduled event. Contact your invitees or update your calendar event to add the details. You can either <3> change your location on the event type or try <5>removing and adding the app again.", - "broken_calendar_action": "We could not update your <1>{{calendar}}. <2> Please check your calendar settings or remove and add your calendar again ", - "attendee_name": "Attendee's name", - "scheduler_full_name": "The full name of the person booking", - "broken_integration": "Broken integration", - "problem_adding_video_link": "There was a problem adding a video link", - "problem_updating_calendar": "There was a problem updating your calendar", - "active_on_event_types_one": "Active on {{count}} event type", - "active_on_event_types_other": "Active on {{count}} event types", - "no_active_event_types": "No active event types", - "new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}", - "new_seat_title": "Someone has added themselves to an event", - "variable": "Variable", - "event_name_variable": "Event name", - "attendee_name_variable": "Attendee", - "event_date_variable": "Event date", - "event_time_variable": "Event time", - "timezone_variable": "Timezone", - "location_variable": "Location", - "additional_notes_variable": "Additional notes", - "organizer_name_variable": "Organizer name", - "app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.", - "invalid_number": "Invalid phone number", - "navigate": "Navigate", - "open": "Open", - "close": "Close", - "upgrade": "Upgrade", - "upgrade_to_access_recordings_title": "Upgrade to access recordings", - "upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls", - "recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan", - "team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.", - "team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.", - "show_eventtype_on_profile": "Show on Profile", - "embed": "Embed", - "new_username": "New username", - "current_username": "Current username", - "example_1": "Example 1", - "example_2": "Example 2", - "booking_question_identifier": "Booking Question Identifier", - "company_size": "Company size", - "what_help_needed": "What do you need help with?", - "variable_format": "Variable format", - "webhook_subscriber_url_reserved": "Webhook subscriber url is already defined", - "custom_input_as_variable_info": "Ignore all special characters of the additional input label (use only letters and numbers), use uppercase for all letters and replace whitespaces with underscores.", - "using_booking_questions_as_variables": "How do I use booking questions as variables?", - "download_desktop_app": "Download desktop app", - "set_ping_link": "Set Ping link", - "rate_limit_exceeded": "Rate limit exceeded", - "when_something_happens": "When something happens", - "action_is_performed": "An action is performed", - "test_action": "Test action", - "notification_sent": "Notification sent", - "no_input": "No input", - "test_workflow_action": "Test workflow action", - "send_sms": "Send SMS", - "send_sms_to_number": "Are you sure you want to send a SMS to {{number}}?", - "missing_connected_calendar": "No default calendar connected", - "connect_your_calendar_and_link": "You can connect your calendar from <1>here.", - "default_calendar_selected": "Default calendar", - "hide_from_profile": "Hide from profile", - "event_setup_tab_title": "Event Setup", - "event_limit_tab_title": "Limits", - "event_limit_tab_description": "How often you can be booked", - "event_advanced_tab_description": "Calendar settings & more...", - "event_advanced_tab_title": "Advanced", - "event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.", - "event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.", - "event_setup_booking_limits_error": "Booking limits must be in ascending order. [day, week, month, year]", - "event_setup_duration_limits_error": "Duration limits must be in ascending order. [day, week, month, year]", - "select_which_cal": "Select which calendar to add bookings to", - "custom_event_name": "Custom event name", - "custom_event_name_description": "Create customised event names to display on calendar event", - "2fa_required": "Two factor authentication required", - "incorrect_2fa": "Incorrect two factor authentication code", - "which_event_type_apply": "Which event type will this apply to?", - "no_workflows_description": "Workflows enable simple automation to send notifications & reminders enabling you to build processes around your events.", "timeformat_profile_hint": "នេះគឺជាការកំណត់ខាងក្នុង ហើយនឹងមិនប៉ះពាល់ដល់របៀបដែលពេលវេលាត្រូវបានបង្ហាញនៅលើទំព័រកក់សាធារណៈសម្រាប់អ្នក ឬនរណាម្នាក់ដែលកក់អ្នក។", - "create_workflow": "Create a workflow", - "do_this": "Do this", - "turn_off": "Turn off", - "turn_on": "Turn on", - "settings_updated_successfully": "Settings updated successfully", - "error_updating_settings": "Error updating settings", - "personal_cal_url": "My personal {{appName}} URL", - "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.", - "bio":"Bio", - "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", "start_of_week": "ការចាប់ផ្តើមនៃសប្តាហ៍", - "recordings_title": "Recordings", - "recording": "Recording", - "happy_scheduling": "Happy scheduling", - "select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.", - "check_for_conflicts": "Check for conflicts", - "view_recordings": "View recordings", - "adding_events_to": "Adding events to", - "follow_system_preferences": "Follow system preferences", - "custom_brand_colors": "Custom brand colors", - "customize_your_brand_colors": "Customize your own brand colour into your booking page.", - "pro": "Pro", - "removes_cal_branding": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}.'", - "profile_picture": "Profile Picture", - "upload": "Upload", - "add_profile_photo": "Add profile photo", - "web3": "Web3", - "token_address": "Token Address", - "blockchain": "Blockchain", - "old_password": "Old password", - "secure_password": "Your new super secure password", - "error_updating_password": "Error updating password", - "two_factor_auth": "Two factor authentication", - "recurring_event_tab_description": "Set up a repeating schedule", "today": "ថ្ងៃនេះ", "appearance": "រូបរាង", "my_account": "គណនី​របស់ខ្ញុំ", "general": "ទូទៅ", "calendars": "ប្រតិទិន", - "2fa_auth": "Two factor auth", "invoices": "វិក្កយបត្រ", - "embeds": "Embeds", - "impersonation": "Impersonation", - "impersonation_description": "Settings to manage user impersonation", "users": "អ្នកប្រើប្រាស់", "user": "អ្នកប្រើប្រាស់", - "profile_description": "Manage settings for your {{appName}} profile", - "users_description": "Here you can find a list of all users", - "users_listing": "User listing", - "general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។", - "calendars_description": "Configure how your event types interact with your calendars", - "appearance_description": "Manage settings for your booking appearance", - "conferencing_description": "Add your favourite video conferencing apps for your meetings", - "add_conferencing_app": "Add Conferencing App", - "password_description": "Manage settings for your account passwords", - "set_up_two_factor_authentication": "Set up your Two-factor authentication", - "we_just_need_basic_info": "We just need some basic info to get your profile setup.", - "skip": "Skip", - "do_this_later": "Do this later", - "set_availability_getting_started_subtitle_1": "Define ranges of time when you are available", - "set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.", - "connect_calendars_from_app_store": "You can add more calendars from the app store", - "connect_conference_apps": "Connect conference apps", - "connect_calendar_apps": "Connect calendar apps", - "connect_payment_apps": "Connect payment apps", - "connect_automation_apps": "Connect automation apps", - "connect_analytics_apps": "Connect analytics apps", - "connect_other_apps": "Connect other apps", - "connect_web3_apps": "Connect web3 apps", - "connect_messaging_apps": "Connect messaging apps", - "connect_crm_apps": "Connect CRM apps", - "current_step_of_total": "Step {{currentStep}} of {{maxSteps}}", - "add_variable": "Add variable", - "custom_phone_number": "Custom phone number", - "message_template": "Message template", - "email_subject": "Email subject", - "add_dynamic_variables": "Add dynamic text variables", - "event_name_info": "The event type name", - "event_date_info": "The event date", - "event_time_info": "The event start time", - "location_info": "The location of the event", - "additional_notes_info": "The additional notes of booking", - "attendee_name_info": "The person booking's name", - "organizer_name_info": "Organizer’s name", - "to": "To", - "workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully", - "download_responses": "Download responses", - "download_responses_description": "Download all responses to your form in CSV format.", - "download": "Download", - "download_recording": "Download Recording", - "recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download", - "create_your_first_form": "Create your first form", - "create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.", - "create_your_first_webhook": "Create your first Webhook", - "create_your_first_webhook_description": "With Webhooks you can receive meeting data in real-time when something happens in {{appName}}.", - "for_a_maximum_of": "For a maximum of", - "event_one": "event", - "event_other": "events", - "profile_team_description": "Manage settings for your team profile", - "profile_org_description": "Manage settings for your organization profile", - "members_team_description": "Users that are in the group", - "organization_description": "Manage the admins and members in your organization", - "team_url": "Team URL", - "team_members": "Team members", - "more": "More", - "more_page_footer": "We view the mobile application as an extension of the web application. If you are performing any complicated actions, please refer back to the web application.", - "workflow_example_1": "Send SMS reminder 24 hours before event starts to attendee", - "workflow_example_2": "Send custom SMS when event is rescheduled to attendee", - "workflow_example_3": "Send custom email when new event is booked to host", - "workflow_example_4": "Send email reminder 1 hour before events starts to attendee", - "workflow_example_5": "Send custom email when event is rescheduled to host", - "workflow_example_6": "Send custom SMS when new event is booked to host", - "welcome_to_cal_header": "Welcome to {{appName}}!", - "edit_form_later_subtitle": "You’ll be able to edit this later.", - "connect_calendar_later": "I'll connect my calendar later", - "problem_saving_user_profile": "There was a problem saving your data. Please try again or reach out to customer support.", - "purchase_missing_seats": "Purchase missing seats", - "slot_length": "Slot length", - "booking_appearance": "Booking Appearance", - "appearance_team_description": "Manage settings for your team's booking appearance", - "only_owner_change": "Only the owner of this team can make changes to the team's booking ", - "team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'", - "invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}", - "token_invalid_expired": "Token is either invalid or expired.", - "exchange_add": "Connect to Microsoft Exchange", - "exchange_authentication": "Authentication method", - "exchange_authentication_standard": "Basic authentication", - "exchange_authentication_ntlm": "NTLM authentication", - "exchange_compression": "GZip compression", - "exchange_version": "Exchange Version", - "exchange_version_2007_SP1": "2007 SP1", - "exchange_version_2010": "2010", - "exchange_version_2010_SP1": "2010 SP1", - "exchange_version_2010_SP2": "2010 SP2", - "exchange_version_2013": "2013", - "exchange_version_2013_SP1": "2013 SP1", - "exchange_version_2015": "2015", - "exchange_version_2016": "2016", - "routing_forms_description": "Create forms to direct attendees to the correct destinations", - "routing_forms_send_email_owner": "Send Email to Owner", - "routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted", - "add_new_form": "Add new form", - "add_new_team_form": "Add new form to your team", - "create_your_first_route": "Create your first route", - "route_to_the_right_person": "Route to the right person based on the answers to your form", - "form_description": "Create your form to route a booker", - "copy_link_to_form": "Copy link to form", - "theme": "Theme", - "theme_applies_note": "This only applies to your public booking pages", - "theme_system": "System default", - "add_a_team": "Add a team", - "add_webhook_description": "Receive meeting data in real-time when something happens in {{appName}}", - "triggers_when": "Triggers when", - "test_webhook": "Please ping test before creating.", - "enable_webhook": "Enable Webhook", - "add_webhook": "Add Webhook", - "webhook_edited_successfully": "Webhook saved", - "api_keys_description": "Generate API keys for accessing your own account", - "new_api_key": "New API key", - "active": "active", - "api_key_updated": "API key name updated", - "api_key_update_failed": "Error updating API key name", - "embeds_title": "HTML iframe embed", - "embeds_description": "Embed all your event types on your website", - "create_first_api_key": "Create your first API key", - "create_first_api_key_description": "API keys allow other apps to communicate with {{appName}}", - "back_to_signin": "Back to sign in", - "reset_link_sent": "Reset link sent", - "password_reset_email": "An email is on it’s way to {{email}} with instructions to reset your password.", - "password_reset_leading": "If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.", - "password_updated": "Password updated!", - "pending_payment": "Pending payment", - "pending_invites": "Pending Invites", - "not_on_cal": "Not on {{appName}}", - "no_calendar_installed": "No calendar installed", - "no_calendar_installed_description": "You have not yet connected any of your calendars", - "add_a_calendar": "Add a calendar", - "change_email_hint": "You may need to log out and back in to see any change take effect", - "confirm_password_change_email": "Please confirm your password before changing your email address", - "seats": "seats", - "every_app_published": "Every app published on the {{appName}} App Store is open source and thoroughly tested via peer reviews. Nevertheless, {{companyName}} does not endorse or certify these apps unless they are published by {{appName}}. If you encounter inappropriate content or behaviour please report it.", - "report_app": "Report app", - "limit_booking_frequency": "Limit booking frequency", - "limit_booking_frequency_description": "Limit how many times this event can be booked", - "limit_total_booking_duration": "Limit total booking duration", - "limit_total_booking_duration_description": "Limit total amount of time that this event can be booked", - "add_limit": "Add Limit", - "team_name_required": "Team name required", - "show_attendees": "Share attendee information between guests", - "show_available_seats_count": "Show the number of available seats", - "how_booking_questions_as_variables": "How to use booking questions as variables?", - "format": "Format", - "uppercase_for_letters": "Use uppercase for all letters", - "replace_whitespaces_underscores": "Replace whitespaces with underscores", - "manage_billing": "Manage billing", - "manage_billing_description": "Manage all things billing", - "billing_freeplan_title": "You're currently on the FREE plan", - "billing_freeplan_description": "We work better in teams. Extend your workflows with round-robin and collective events and make advanced routing forms", - "billing_freeplan_cta": "Try now", - "billing_portal": "Billing portal", - "billing_help_cta": "Contact support", - "ignore_special_characters_booking_questions": "Ignore special characters in your booking question identifier. Use only letters and numbers", - "retry": "Retry", - "fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again or reach out to customer support.", - "calendar_connection_fail": "Calendar connection failed", - "booking_confirmation_success": "Booking confirmation succeeded", - "booking_rejection_success": "Booking rejection succeeded", - "booking_tentative": "This booking is tentative", - "booking_accept_intent": "Oops, I want to accept", - "we_wont_show_again": "We won't show this again", - "couldnt_update_timezone": "We couldn't update the timezone", - "updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}", - "update_timezone": "Update timezone", - "update_timezone_question": "Update Timezone?", - "update_timezone_description": "It seems like your local timezone has changed to {{formattedCurrentTz}}. It's very important to have the correct timezone to prevent bookings at undesired times. Do you want to update it?", - "dont_update": "Don't update", - "require_additional_notes": "Require additional notes", - "require_additional_notes_description": "Require additional notes to be filled out when booking", - "email_address_action": "send email to a specific email address", - "after_event_trigger": "after event ends", - "how_long_after": "How long after event ends?", - "no_available_slots": "No Available slots", - "time_available": "Time available", - "cant_find_the_right_video_app_visit_our_app_store": "Can't find the right video app? Visit our <1>App Store.", - "install_new_calendar_app": "Install new calendar app", - "make_phone_number_required": "Make phone number required for booking event", - "new_event_type_availability": "{{eventTypeTitle}} Availability", - "error_editing_availability": "Error editing availability", - "dont_have_permission": "You don't have permission to access this resource.", - "saml_config": "SAML", - "saml_configuration_placeholder": "Please paste the SAML metadata from your Identity Provider here", - "saml_email_required": "Please enter an email so we can find your SAML Identity Provider", - "saml_sp_title": "Service Provider Details", - "saml_sp_description": "Your Identity Provider (IdP) will ask you for the following details to complete the SAML application configuration.", - "saml_sp_acs_url": "ACS URL", - "saml_sp_entity_id": "SP Entity ID", - "saml_sp_acs_url_copied": "ACS URL copied!", - "saml_sp_entity_id_copied": "SP Entity ID copied!", - "add_calendar": "Add Calendar", - "limit_future_bookings": "Limit future bookings", - "limit_future_bookings_description": "Limit how far in the future this event can be booked", - "no_event_types": "No event types setup", - "no_event_types_description": "{{name}} has not setup any event types for you to book.", - "billing_frequency": "Billing Frequency", - "monthly": "Monthly", - "yearly": "Yearly", - "checkout": "Checkout", - "your_team_disbanded_successfully": "Your team has been disbanded successfully", - "your_org_disbanded_successfully": "Your organization has been disbanded successfully", - "error_creating_team": "Error creating team", - "you": "You", - "resend_email": "Resend email", - "member_already_invited": "Member has already been invited", - "enter_email_or_username": "Enter an email or username", - "team_name_taken": "This name is already taken", - "must_enter_team_name": "Must enter a team name", - "team_url_required": "Must enter a team URL", - "url_taken": "This URL is already taken", - "problem_registering_domain": "There was a problem with registering the subdomain, please try again or contact an administrator", - "team_publish": "Publish team", - "number_text_notifications": "Phone number (Text notifications)", - "number_sms_notifications": "Phone number (SMS notifications)", - "attendee_email_variable": "Attendee email", - "attendee_email_info": "The person booking's email", - "kbar_search_placeholder": "Type a command or search...", - "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", - "reschedule_reason": "Reschedule reason", - "choose_common_schedule_team_event": "Choose a common schedule", - "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", - "reason": "Reason", - "sender_id": "Sender ID", - "sender_id_error_message": "Only letters, numbers and spaces allowed (max. 11 characters)", - "test_routing_form": "Test Routing Form", - "test_preview": "Test Preview", - "route_to": "Route to", - "test_preview_description": "Test your routing form without submitting any data", - "test_routing": "Test Routing", - "payment_app_disabled": "An admin has disabled a payment app", - "edit_event_type": "Edit event type", - "collective_scheduling": "Collective Scheduling", - "make_it_easy_to_book": "Make it easy to book your team when everyone is available.", - "find_the_best_person": "Find the best person available and cycle through your team.", - "fixed_round_robin": "Fixed round robin", - "add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.", - "calcom_is_better_with_team": "{{appName}} is better with teams", - "the_calcom_team": "The {{companyName}} team", - "add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.", - "booking_limit_reached": "Booking Limit for this event type has been reached", - "duration_limit_reached": "Duration Limit for this event type has been reached", - "admin_has_disabled": "An admin has disabled {{appName}}", - "disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}", - "event_replaced_notice": "An admin has replaced one of your event types", - "email_subject_slug_replacement": "A team administrator has replaced your event /{{slug}}", - "email_body_slug_replacement_notice": "An administrator on the {{teamName}} team has replaced your event type /{{slug}} with a managed event type that they control.", - "email_body_slug_replacement_info": "Your link will continue to work but some settings for it may have changed. You can review it in event types.", - "email_body_slug_replacement_suggestion": "If you have any questions about the event type, please reach out to your administrator.

Happy scheduling,
The Cal.com team", - "disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.", - "payment_disabled_still_able_to_book": "Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin reenables your payment method.", - "app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.", - "app_disabled_video": "The admin has disabled {{appName}} which may affect your event types. If you have event types with {{appName}} as the location then it will default to Cal Video.", - "app_disabled_subject": "{{appName}} has been disabled", - "navigate_installed_apps": "Go to installed apps", - "disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.", - "enable_apps": "Enable Apps", - "enable_apps_description": "Enable apps that users can integrate with {{appName}}", - "purchase_license": "Purchase a License", - "already_have_key": "I already have a key:", - "already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.", - "app_is_enabled": "{{appName}} is enabled", - "app_is_disabled": "{{appName}} is disabled", - "keys_have_been_saved": "Keys have been saved", - "disable_app": "Disable App", - "disable_app_description": "Disabling this app could cause problems with how your users interact with Cal", - "edit_keys": "Edit Keys", - "admin_apps_description": "Enable apps for your instance of Cal", - "no_available_apps": "There are no available apps", - "no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'", - "no_apps": "There are no apps enabled in this instance of Cal", - "no_apps_configured": "No app has been configured yet", - "enable_in_settings": "You can enable apps in the settings", - "please_contact_admin": "Please contact your admin", - "apps_settings": "Apps settings", - "fill_this_field": "Please fill in this field", - "options": "Options", - "enter_option": "Enter Option {{index}}", - "add_an_option": "Add an option", - "radio": "Radio", - "google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar", - "individual": "Individual", - "all_bookings_filter_label": "All Bookings", - "all_users_filter_label": "All Users", - "your_bookings_filter_label": "Your Bookings", - "meeting_url_variable": "Meeting url", - "meeting_url_info": "The event meeting conference url", - "date_overrides": "Date overrides", - "date_overrides_subtitle": "Add dates when your availability changes from your daily hours.", - "date_overrides_info": "Date overrides are archived automatically after the date has passed", - "date_overrides_dialog_which_hours": "Which hours are you free?", - "date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?", - "date_overrides_dialog_title": "Select the dates to override", - "date_overrides_unavailable": "Unavailable all day", - "date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)", - "date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates", - "date_overrides_add_btn": "Add Override", - "date_overrides_update_btn": "Update Override", - "date_successfully_added": "Date override added successfully", - "event_type_duplicate_copy_text": "{{slug}}-copy", - "set_as_default": "Set as default", - "hide_eventtype_details": "Hide event type details", - "show_navigation": "Show navigation", - "hide_navigation": "Hide navigation", - "verification_code_sent": "Verification code sent", - "verified_successfully": "Verified successfully", - "wrong_code": "Wong verification code", - "not_verified": "Not yet verified", - "no_availability_in_month": "No availability in {{month}}", - "view_next_month": "View next month", - "send_code": "Send code", - "number_verified": "Number Verified", - "create_your_first_team_webhook_description": "Create your first webhook for this team event type", - "create_webhook_team_event_type": "Create a webhook for this team event type", - "disable_success_page": "Disable Success Page (only works if you have a redirect URL)", - "invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet", - "change_password_admin": "Change Password to gain admin access", - "username_already_taken": "Username is already taken", - "assignment": "Assignment", - "fixed_hosts": "Fixed Hosts", - "add_fixed_hosts": "Add fixed hosts", - "round_robin_hosts": "Round-Robin Hosts", - "minimum_round_robin_hosts_count": "Number of hosts required to attend", - "hosts": "Hosts", - "upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.", - "orgs_upgrade_to_enable_feature" : "You need to upgrade to our enterprise plan to enable this feature.", - "new_attendee": "New Attendee", - "awaiting_approval": "Awaiting Approval", - "requires_google_calendar": "This app requires a Google Calendar connection", - "connected_google_calendar": "You have connected a Google Calendar account.", - "using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar", - "continue_to_install_google_calendar": "Continue to install Google Calendar", - "install_google_meet": "Install Google Meet", - "install_google_calendar": "Install Google Calendar", - "sender_name": "Sender name", - "already_invited": "Attendee already invited", - "no_recordings_found": "No recordings found", - "new_workflow_subtitle": "New workflow for...", - "reporting": "Reporting", - "reporting_feature": "See all incoming form data and download it as a CSV", - "teams_plan_required": "Teams plan required", - "routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.", - "choose_a_license": "Choose a license", - "choose_license_description": "Cal.com comes with an accessible and free AGPLv3 license which has limitations. We are onboarding Enterprise customers for the commercial license which you can inquire about by contacting sales below.", - "license": "License", - "agplv3_license": "AGPLv3 License", - "no_need_to_keep_your_code_open_source": "No need to keep your code open source", - "repackage_rebrand_resell": "Repackage, rebrand and resell easily", - "a_vast_suite_of_enterprise_features": "A vast suite of enterprise features", - "free_license_fee": "$0.00/month", - "forever_open_and_free": "Forever Open & Free", - "required_to_keep_your_code_open_source": "Required to keep your code open source", - "cannot_repackage_and_resell": "Cannot repackage, rebrand and resell easily", - "no_enterprise_features": "No enterprise features", - "step_enterprise_license": "Enterprise License", - "step_enterprise_license_description": "Everything for a commercial use case with private hosting, repackaging, rebranding and reselling and access exclusive enterprise components.", - "setup": "Setup", - "setup_description": "Setup Cal.com instance", - "configure": "Configure", - "sso_configuration": "Single Sign-On", - "sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider", - "sso_oidc_heading": "SSO with OIDC", - "sso_oidc_description": "Configure OIDC SSO with Identity Provider of your choice.", - "sso_oidc_configuration_title": "OIDC Configuration", - "sso_oidc_configuration_description": "Configure OIDC connection to your identity provider. You can find the required information in your identity provider.", - "sso_oidc_callback_copied": "Callback URL copied", - "sso_saml_heading": "SSO with SAML", - "sso_saml_description": "Configure SAML SSO with Identity Provider of your choice.", - "sso_saml_configuration_title": "SAML Configuration", - "sso_saml_configuration_description": "Configure SAML connection to your identity provider. You can find the required information in your identity provider.", - "sso_saml_acsurl_copied": "ACS URL copied", - "sso_saml_entityid_copied": "Entity ID copied", - "sso_connection_created_successfully": "{{connectionType}} configuration created successfully", - "sso_connection_deleted_successfully": "{{connectionType}} configuration deleted successfully", - "delete_sso_configuration": "Delete {{connectionType}} configuration", - "delete_sso_configuration_confirmation": "Yes, delete {{connectionType}} configuration", - "delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.", - "organizer_timezone": "Organizer timezone", - "email_user_cta": "View Invitation", - "email_no_user_invite_heading_team": "You’ve been invited to join a {{appName}} team", - "email_no_user_invite_heading_org": "You’ve been invited to join a {{appName}} organization", - "email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", - "email_user_invite_subheading_team": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", - "email_user_invite_subheading_org": "{{invitedBy}} has invited you to join their organization `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your organization to schedule meetings without the email tennis.", - "email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your {{entity}} in no time.", - "email_no_user_step_one": "Choose your username", - "email_no_user_step_two": "Connect your calendar account", - "email_no_user_step_three": "Set your Availability", - "email_no_user_step_four": "Join {{teamName}}", - "email_no_user_signoff": "Happy Scheduling from the {{appName}} team", - "impersonation_user_tip": "You are about to impersonate a user, which means you can make changes on their behalf. Please be careful.", - "available_variables": "Available variables", - "scheduler": "{Scheduler}", - "no_workflows": "No workflows", - "change_filter": "Change filter to see your personal and team workflows.", - "change_filter_common": "Change filter to see the results.", - "no_results_for_filter": "No results for the filter", - "recommended_next_steps": "Recommended next steps", - "create_a_managed_event": "Create a managed event type", - "meetings_are_better_with_the_right": "Meetings are better with the right team members there. Invite them now.", - "create_a_one_one_template": "Create a one-one one template for an event type and distribute it to multiple members.", - "collective_or_roundrobin": "Collective or round-robin", - "book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.", - "event_no_longer_attending_subject": "No longer attending {{title}} at {{date}}", - "no_longer_attending": "You are no longer attending this event", - "attendee_no_longer_attending_subject": "An attendee is no longer attending {{title}} at {{date}}", - "attendee_no_longer_attending": "An attendee is no longer attending your event", - "attendee_no_longer_attending_subtitle": "{{name}} has canceled. This means a seat has opened up for this time slot", - "create_event_on": "Create event on", - "create_routing_form_on": "Create routing form on", - "default_app_link_title": "Set a default app link", - "default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.", - "organizer_default_conferencing_app": "Organizer's default app", - "under_maintenance": "Down for maintenance", - "under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.", - "event_type_seats": "{{numberOfSeats}} seats", - "booking_questions_title": "Booking questions", - "booking_questions_description": "Customize the questions asked on the booking page", - "add_a_booking_question": "Add a question", - "identifier": "Identifier", - "duplicate_email": "Email is duplicate", - "booking_with_payment_cancelled": "Paying for this event is no longer possible", - "booking_with_payment_cancelled_already_paid": "A refund for this booking payment is on its way.", - "booking_with_payment_cancelled_refunded": "This booking payment has been refunded.", - "booking_confirmation_failed": "Booking confirmation failed", - "not_enough_seats": "Not enough seats", - "form_builder_field_already_exists": "A field with this name already exists", - "show_on_booking_page": "Show on booking page", - "get_started_zapier_templates": "Get started with Zapier templates", - "team_is_unpublished": "{{team}} is unpublished", - "org_is_unpublished_description": "This organization link is currently not available. Please contact the organization owner or ask them to publish it.", - "team_is_unpublished_description": "This team link is currently not available. Please contact the team owner or ask them to publish it.", - "team_member": "Team member", - "a_routing_form": "A Routing Form", - "form_description_placeholder": "Form Description", - "keep_me_connected_with_form": "Keep me connected with the form", - "fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.", - "form_deleted": "Form deleted", - "delete_form": "Are you sure you want to delete this form?", - "delete_form_action": "Yes, delete Form", - "delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.", - "delete_form_confirmation_2": "All associated responses will be deleted.", - "typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.", - "modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.", - "connected_forms": "Connected Forms", - "form_modifications_warning": "Following forms would be affected when you modify fields or routes here.", - "responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.", - "this_is_what_your_users_would_see": "This is what your users would see", - "identifies_name_field": "Identifies field by this name.", - "add_1_option_per_line": "Add 1 option per line", - "select_a_router": "Select a router", - "add_a_new_route": "Add a new Route", - "make_informed_decisions": "Make informed decisions with Insights", - "make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.", - "view_bookings_across": "View bookings across all members", - "view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team", - "identify_booking_trends": "Identify booking trends", - "identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers", - "spot_popular_event_types": "Spot popular event types", - "spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings", - "no_responses_yet": "No responses yet", - "no_routes_defined": "No routes defined", - "this_will_be_the_placeholder": "This will be the placeholder", - "error_booking_event": "An error occurred when booking the event, please refresh the page and try again", - "timeslot_missing_title": "No timeslot selected", - "timeslot_missing_description": "Please select a timeslot to book the event.", - "timeslot_missing_cta": "Select timeslot", - "switch_monthly": "Switch to monthly view", - "switch_weekly": "Switch to weekly view", - "switch_multiday": "Switch to day view", - "switch_columnview": "Switch to column view", - "num_locations": "{{num}} location options", - "select_on_next_step": "Select on the next step", - "this_meeting_has_not_started_yet": "This meeting has not started yet", - "this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account", - "connect_app": "Connect {{dependencyName}}", - "app_is_connected": "{{dependencyName}} is connected", - "requires_app": "Requires {{dependencyName}}", - "verification_code": "Verification code", - "can_you_try_again": "Can you try again with a different time?", - "verify": "Verify", - "timezone_info": "The timezone of the person receiving", - "event_end_time_variable": "Event end time", - "event_end_time_info": "The event end time", - "cancel_url_variable": "Cancel URL", - "cancel_url_info": "The URL to cancel the booking", - "reschedule_url_variable": "Reschedule URL", - "reschedule_url_info": "The URL to reschedule the booking", - "invalid_event_name_variables": "There is an invalid variable in your event name", - "select_all": "Select All", - "default_conferencing_bulk_title": "Bulk update existing event types", - "members_default_schedule": "Member's default schedule", - "set_by_admin": "Set by team admin", - "members_default_location": "Member's default location", - "members_default_schedule_description": "We will use each members default availability schedule. They will be able to edit or change it.", - "requires_at_least_one_schedule": "You are required to have at least one schedule", - "default_conferencing_bulk_description": "Update the locations for the selected event types", - "locked_for_members": "Locked for members", - "locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings", - "locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings", - "locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings", - "locked_by_admin": "Locked by team admin", - "app_not_connected": "You have not connected a {{appName}} account.", - "connect_now": "Connect now", - "managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member", - "managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members", - "managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?", - "managed_event_dialog_title_other": "The url /{{slug}} already exists for {{count}} members. Do you want to replace it?", - "managed_event_dialog_information_one": "{{names}} is already using the /{{slug}} url.", - "managed_event_dialog_information_other": "{{names}} are already using the /{{slug}} url.", - "managed_event_dialog_clarification": "If you choose to replace it, we will notify them. Go back and remove them if you don't want to overwrite it.", - "review_event_type": "Review Event Type", - "looking_for_more_analytics": "Looking for more analytics?", - "looking_for_more_insights": "Looking for more Insights?", - "add_filter": "Add filter", - "select_user": "Select User", - "select_event_type": "Select Event Type", - "select_date_range": "Select Date Range", - "popular_events": "Popular Events", - "no_event_types_found": "No event types found", - "average_event_duration": "Average Event Duration", - "most_booked_members": "Most Booked Members", - "least_booked_members": "Least Booked Members", - "events_created": "Events Created", - "events_completed": "Events Completed", - "events_cancelled": "Events Canceled", - "events_rescheduled": "Events Rescheduled", - "from_last_period": "from last period", - "from_to_date_period": "From: {{startDate}} To: {{endDate}}", - "redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.", - "event_trends": "Event Trends", - "clear_filters": "Clear Filters", - "clear": "Clear", - "hold": "Hold", - "on_booking_option": "Collect payment on booking", - "hold_option": "Charge no-show fee", - "card_held": "Card held", - "charge_card": "Charge card", - "card_charged": "Card charged", - "no_show_fee_amount": "{{amount, currency}} no-show fee", - "no_show_fee": "No-show Fee", - "submit_card": "Submit card", - "submit_payment_information": "Submit payment information", - "meeting_awaiting_payment_method": "Your meeting is awaiting a payment method", - "no_show_fee_charged_email_subject": "No-show fee of {{amount, currency}} charged for {{title}} at {{date}}", - "no_show_fee_charged_text_body": "No-show fee was charged", - "no_show_fee_charged_subtitle": "No-show fee of {{amount, currency}} was charged for the following event", - "error_charging_card": "Something went wrong charging the no-show fee. Please try again later.", - "collect_no_show_fee": "Collect no-show fee", - "no_show_fee_charged": "No-show fee charged", - "insights": "Insights", - "testing_workflow_info_message": "When testing this workflow, be aware that Emails and SMS can only be scheduled at least 1 hour in advance", - "insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.", - "acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.", - "card_details": "Card details", - "something_went_wrong_on_our_end":"Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right away for you.", - "please_provide_following_text_to_suppport":"Please provide the following text when contacting support to better help you", - "seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee", - "complete_your_booking": "Complete your booking", - "complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}", - "confirm_your_details": "Confirm your details", - "copy_invite_link": "Copy invite link", - "edit_invite_link": "Edit link settings", - "invite_link_copied": "Invite link copied", - "invite_link_deleted": "Invite link deleted", - "api_key_deleted":"API Key deleted", - "invite_link_updated": "Invite link settings saved", - "link_expires_after": "Links set to expire after...", - "one_day": "1 day", - "seven_days": "7 days", - "thirty_days": "30 days", - "three_months": "3 months", - "one_year": "1 year", - "team_invite_received": "You have been invited to join {{teamName}}", - "currency_string": "{{amount, currency}}", - "charge_card_dialog_body": "You are about to charge the attendee {{amount, currency}}. Are you sure you want to continue?", - "charge_attendee": "Charge attendee {{amount, currency}}", - "payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)", - "email_invite_team": "{{email}} has been invited", - "email_invite_team_bulk": "{{userCount}} users have been invited", - "error_collecting_card": "Error collecting card", - "image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit", - "unauthorized_workflow_error_message": "{{errorCode}}: You are not authorized to enable or disable this workflow", - "inline_embed": "Inline Embed", - "load_inline_content": "Loads your event type directly inline with your other website content.", - "floating_pop_up_button": "Floating pop-up button", - "floating_button_trigger_modal": "Puts a floating button on your site that triggers a modal with your event type.", - "pop_up_element_click": "Pop up via element click", - "open_dialog_with_element_click": "Open your calendar as a dialog when someone clicks an element.", - "need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.", - "book_my_cal": "Book my Cal", - "first_name": "First name", - "last_name": "Last name", - "first_last_name": "First name, Last name", - "invite_as": "Invite as", - "form_updated_successfully": "Form updated successfully.", - "disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees", - "disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.", - "disable_host_confirmation_emails": "Disable default confirmation emails for host", - "disable_host_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the host when the event is booked.", - "add_an_override": "Add an override", - "import_from_google_workspace": "Import users from Google Workspace", - "connect_google_workspace": "Connect Google Workspace", - "google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature", - "first_event_type_webhook_description": "Create your first webhook for this event type", - "install_app_on": "Install app on", - "create_for": "Create for", - "currency": "Currency", - "organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.", - "organization_banner_title": "Manage organizations with multiple teams", - "set_up_your_organization": "Set up your organization", - "organizations_description": "Organizations are shared environments where teams can create shared event types, apps, workflows and more.", - "must_enter_organization_name": "Must enter an organization name", - "must_enter_organization_admin_email": "Must enter your organization email address", - "admin_email": "Your organization email address", - "admin_username": "Administrator's username", - "organization_name": "Organization name", - "organization_url": "Organization URL", - "organization_verify_header": "Verify your organization email", - "organization_verify_email_body": "Please use the code below to verify your email address to continue setting up your organization.", - "additional_url_parameters": "Additional URL parameters", - "about_your_organization": "About your organization", - "about_your_organization_description": "Organizations are shared environments where you can create multiple teams with shared members, event types, apps, workflows and more.", - "create_your_teams": "Create your teams", - "create_your_teams_description": "Start scheduling together by adding your team members to your organization", - "invite_organization_admins": "Invite your organization admins", - "invite_organization_admins_description": "These admins will have access to all teams in your organization. You can add team admins and members later.", - "set_a_password": "Set a password", - "set_a_password_description": "This will create a new user account with your organization email and this password.", - "organization_logo": "Organization Logo", - "organization_about_description": "A few sentences about your organization. This will appear on your organization public profile page.", - "ill_do_this_later": "I'll do this later", - "verify_your_email": "Verify your email", - "enter_digit_code": "Enter the 6 digit code we sent to {{email}}", - "verify_email_organization": "Verify your email to create an organization", - "code_provided_invalid": "The code provided is not valid, try again", - "email_already_used": "Email already being used", - "organization_admin_invited_heading":"You've been invited to join {{orgName}}", - "organization_admin_invited_body":"Join your team at {{orgName}} and start focusing on meeting, not making meetings!", - "duplicated_slugs_warning": "The following teams couldn't be created due to duplicated slugs: {{slugs}}", - "team_names_empty": "Team names can't be empty", - "team_names_repeated": "Team names can't be repeated", - "user_belongs_organization": "User belongs to an organization", - "org_no_teams_yet": "This organization has no teams yet", - "org_no_teams_yet_description": "If you are an administrator, be sure to create teams to be shown here.", - "set_up": "Set up", - "my_profile": "My Profile", - "my_settings": "My Settings", - "crm": "CRM", - "messaging": "Messaging", - "sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)", - "org_admins_can_create_new_teams": "Only the admin of your organization can create new teams", - "google_new_spam_policy": "Google’s new spam policy could prevent you from receiving any email and calendar notifications about this booking.", - "resolve": "Resolve", - "no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.", - "org_name": "Organization name", - "org_url": "Organization URL", - "copy_link_org": "Copy link to organization", - "404_the_org": "The organization", - "404_the_team": "The team", - "404_claim_entity_org": "Claim your subdomain for your organization", - "404_claim_entity_team": "Claim this team and start managing schedules collectively", - "insights_all_org_filter": "All", - "insights_team_filter": "Team: {{teamName}}", - "insights_user_filter": "User: {{userName}}", - "insights_subtitle": "View booking insights across your events", - "location_options": "{{locationCount}} location options", - "custom_plan": "Custom Plan", - "email_embed": "Email Embed", - "add_times_to_your_email": "Select a few available times and embed them in your Email", - "select_time": "Select Time", - "select_date": "Select Date", - "see_all_available_times": "See all available times", - "org_team_names_example": "e.g. Marketing Team", - "org_team_names_example_1": "e.g. Marketing Team", - "org_team_names_example_2": "e.g. Sales Team", - "org_team_names_example_3": "e.g. Design Team", - "org_team_names_example_4": "e.g. Engineering Team", - "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.", - "organizations": "Organizations", - "org_admin_other_teams": "Other teams", - "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", - "attendee_last_name_variable": "Attendee last name", - "attendee_first_name_info": "The person booking's first name", - "attendee_last_name_info": "The person booking's last name", - "your_monthly_digest": "Your Monthly Digest", - "member_name": "Member Name", - "most_popular_events": "Most Popular Events", - "summary_of_events_for_your_team_for_the_last_30_days": "Here's your summary of popular events for your team {{teamName}} for the last 30 days", - "me": "Me", - "monthly_digest_email":"Monthly Digest Email", - "monthly_digest_email_for_teams": "Monthly digest email for teams", - "verify_team_tooltip": "Verify your team to enable sending messages to attendees", - "member_removed": "Member removed", - "my_availability": "My Availability", - "team_availability": "Team Availability", - "backup_code": "Backup Code", - "backup_codes": "Backup Codes", - "backup_code_instructions": "Each backup code can be used exactly once to grant access without your authenticator.", - "backup_codes_copied": "Backup codes copied!", - "incorrect_backup_code": "Backup code is incorrect.", - "lost_access": "Lost access", - "missing_backup_codes": "No backup codes found. Please generate them in your settings.", - "admin_org_notification_email_subject": "New organization created: pending action", - "hi_admin": "Hi Administrator", - "admin_org_notification_email_title": "An organization requires DNS setup", - "admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.

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.

Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.

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", - "team_no_event_types": "This team has no event types", - "seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations", - "include_calendar_event": "Include calendar event", - "oAuth": "OAuth", - "recently_added":"Recently added", - "no_members_found": "No members found", - "event_setup_length_error":"Event Setup: The duration must be at least 1 minute.", - "availability_schedules":"Availability Schedules", - "unauthorized":"Unauthorized", - "access_cal_account": "{{clientName}} would like access to your {{appName}} account", - "select_account_team": "Select account or team", - "allow_client_to": "This will allow {{clientName}} to", - "associate_with_cal_account":"Associate you with your personal info from {{clientName}}", - "see_personal_info":"See your personal info, including any personal info you've made publicly available", - "see_primary_email_address":"See your primary email address", - "connect_installed_apps":"Connect to your installed apps", - "access_event_type": "Read, edit, delete your event-types", - "access_availability": "Read, edit, delete your availability", - "access_bookings": "Read, edit, delete your bookings", - "allow_client_to_do": "Allow {{clientName}} to do this?", - "oauth_access_information": "By clicking allow, you allow this app to use your information in accordance with their terms of service and privacy policy. You can remove access in the {{appName}} App Store.", - "allow": "Allow", - "view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.", - "view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.", - "you_can_override_calendar_in_advanced_tab":"You can override this on a per-event basis in Advanced settings in each event type.", - "edit_users_availability":"Edit user's availability: {{username}}", - "resend_invitation": "Resend invitation", - "invitation_resent": "The invitation was resent.", - "add_client": "Add client", - "copy_client_secret_info": "After copying the secret you won't be able to view it anymore", - "add_new_client": "Add new Client", - "this_app_is_not_setup_already": "This app has not been setup yet", - "as_csv": "as CSV", - "overlay_my_calendar":"Overlay my calendar", - "overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.", - "view_overlay_calendar_events":"View your calendar events to prevent clashed booking.", - "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" + "general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។" } From aa54c013f808c703a23886cf29e833e0747e85dc Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:07:30 +0530 Subject: [PATCH 032/118] fix: allow dots in username (#11706) * fix: allow dots in username * test: added unit tests for slugify * test: add test for username change * tests: add test for username and dynamic booking * fix: type error --------- Co-authored-by: Peer Richelsen --- apps/web/playwright/change-username.e2e.ts | 31 +++++++++++++++++++ .../playwright/dynamic-booking-pages.e2e.ts | 2 +- .../utils/bookingScenario/bookingScenario.ts | 2 +- packages/lib/slugify.test.ts | 15 +++++++++ packages/lib/slugify.ts | 8 +++-- 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/apps/web/playwright/change-username.e2e.ts b/apps/web/playwright/change-username.e2e.ts index b2f611714f..46bf03e778 100644 --- a/apps/web/playwright/change-username.e2e.ts +++ b/apps/web/playwright/change-username.e2e.ts @@ -43,9 +43,40 @@ test.describe("Change username on settings", () => { id: user.id, }, }); + expect(newUpdatedUser.username).toBe("demousernamex"); }); + test("User can change username to include periods(or dots)", async ({ page, users, prisma }) => { + const user = await users.create(); + + await user.apiLogin(); + // Try to go homepage + await page.goto("/settings/my-account/profile"); + // Change username from normal to normal + const usernameInput = page.locator("[data-testid=username-input]"); + // User can change username to include dots(or periods) + await usernameInput.fill("demo.username"); + await page.click("[data-testid=update-username-btn]"); + await Promise.all([ + page.click("[data-testid=save-username]"), + page.getByTestId("toast-success").waitFor(), + ]); + await page.waitForLoadState("networkidle"); + + const updatedUser = await prisma.user.findUniqueOrThrow({ + where: { + id: user.id, + }, + }); + + expect(updatedUser.username).toBe("demo.username"); + + // Check if user avatar can be accessed and response headers contain 'image/' in the content type + const response = await page.goto("/demo.username/avatar.png"); + expect(response?.headers()?.["content-type"]).toContain("image/"); + }); + test("User can update to PREMIUM username", async ({ page, users }, testInfo) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed"); diff --git a/apps/web/playwright/dynamic-booking-pages.e2e.ts b/apps/web/playwright/dynamic-booking-pages.e2e.ts index eddb68be20..f41fe4c91b 100644 --- a/apps/web/playwright/dynamic-booking-pages.e2e.ts +++ b/apps/web/playwright/dynamic-booking-pages.e2e.ts @@ -13,7 +13,7 @@ test("dynamic booking", async ({ page, users }) => { const pro = await users.create(); await pro.apiLogin(); - const free = await users.create({ username: "free" }); + const free = await users.create({ username: "free.example" }); await page.goto(`/${pro.username}+${free.username}`); await test.step("book an event first day in next month", async () => { diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index f8ddc1c735..5f95c6afdd 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -635,7 +635,7 @@ export const TestData = { example: { name: "Example", email: "example@example.com", - username: "example", + username: "example.username", defaultScheduleId: 1, timeZone: Timezones["+5:30"], }, diff --git a/packages/lib/slugify.test.ts b/packages/lib/slugify.test.ts index 0cf9760303..634e147c84 100644 --- a/packages/lib/slugify.test.ts +++ b/packages/lib/slugify.test.ts @@ -30,6 +30,21 @@ describe("slugify", () => { expect(slugify("$hello-there_")).toEqual("hello-there"); }); + it("should keep periods as is except the start and end", () => { + expect(slugify("hello.there")).toEqual("hello.there"); + expect(slugify("h.e.l.l.o.t.h.e.r.e")).toEqual("h.e.l.l.o.t.h.e.r.e"); + }); + it("should remove consecutive periods", () => { + expect(slugify("hello...there")).toEqual("hello.there"); + expect(slugify("hello....there")).toEqual("hello.there"); + expect(slugify("hello..there")).toEqual("hello.there"); + }); + it("should remove periods from start and end", () => { + expect(slugify(".hello.there")).toEqual("hello.there"); + expect(slugify(".hello.there.")).toEqual("hello.there"); + expect(slugify("hellothere.")).toEqual("hellothere"); + }); + // This is failing, if we want to fix it, one approach is as used in getValidRhfFieldName it.skip("should remove unicode and emoji characters", () => { expect(slugify("Hello 📚🕯️®️ There")).toEqual("hello---------there"); diff --git a/packages/lib/slugify.ts b/packages/lib/slugify.ts index e43d8d57e3..71cd8a7ac8 100644 --- a/packages/lib/slugify.ts +++ b/packages/lib/slugify.ts @@ -7,11 +7,13 @@ export const slugify = (str: string, forDisplayingInput?: boolean) => { .trim() // Remove whitespace from both sides .normalize("NFD") // Normalize to decomposed form for handling accents .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters - .replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash + .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash - .replace(/^-+/, ""); // Remove dashes from start + .replace(/^-+/, "") // Remove dashes from start + .replace(/\.{2,}/g, ".") // Replace consecutive periods with a single period + .replace(/^\.+/, ""); // Remove periods from the start - return forDisplayingInput ? s : s.replace(/-+$/, ""); // Remove dashes from end + return forDisplayingInput ? s : s.replace(/-+$/, "").replace(/\.*$/, ""); // Remove dashes and period from end }; export default slugify; From df4aa249130fd611bfa0e4da114ed36a31373ef5 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Mon, 23 Oct 2023 13:45:26 +0100 Subject: [PATCH 033/118] chore: improve cal.ai not-installed message (#12022) --- apps/ai/src/app/api/receive/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ai/src/app/api/receive/route.ts b/apps/ai/src/app/api/receive/route.ts index 63d6e5d0e4..68bbc51168 100644 --- a/apps/ai/src/app/api/receive/route.ts +++ b/apps/ai/src/app/api/receive/route.ts @@ -88,7 +88,7 @@ export const POST = async (request: NextRequest) => { // User is not a cal.com user or is using an unverified email. if (!signature || !user) { await sendEmail({ - html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address.`, + html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address and then install Cal.ai here: go.cal.com/ai.`, subject: `Re: ${subject}`, text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`, to: envelope.from, From ce64c494f4c1c0b9d2af1558e23ba4a9d07823c9 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:18:18 +0400 Subject: [PATCH 034/118] fix: Allow passing secret for the webhooks via API (#12039) --- apps/api/lib/validations/webhook.ts | 2 ++ apps/api/pages/api/webhooks/[id]/_patch.ts | 3 +++ apps/api/pages/api/webhooks/_post.ts | 3 +++ 3 files changed, 8 insertions(+) diff --git a/apps/api/lib/validations/webhook.ts b/apps/api/lib/validations/webhook.ts index 91d8560195..71219d2fa0 100644 --- a/apps/api/lib/validations/webhook.ts +++ b/apps/api/lib/validations/webhook.ts @@ -20,6 +20,7 @@ export const schemaWebhookCreateParams = z payloadTemplate: z.string().optional().nullable(), eventTypeId: z.number().optional(), userId: z.number().optional(), + secret: z.string().optional().nullable(), // API shouldn't mess with Apps webhooks yet (ie. Zapier) // appId: z.string().optional().nullable(), }) @@ -31,6 +32,7 @@ export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams .merge( z.object({ eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(), + secret: z.string().optional().nullable(), }) ) .partial() diff --git a/apps/api/pages/api/webhooks/[id]/_patch.ts b/apps/api/pages/api/webhooks/[id]/_patch.ts index 35c2810f39..fd0f8db3f5 100644 --- a/apps/api/pages/api/webhooks/[id]/_patch.ts +++ b/apps/api/pages/api/webhooks/[id]/_patch.ts @@ -51,6 +51,9 @@ import { schemaWebhookEditBodyParams, schemaWebhookReadPublic } from "~/lib/vali * eventTypeId: * type: number * description: The event type ID if this webhook should be associated with only that event type + * secret: + * type: string + * description: The secret to verify the authenticity of the received payload * tags: * - webhooks * externalDocs: diff --git a/apps/api/pages/api/webhooks/_post.ts b/apps/api/pages/api/webhooks/_post.ts index 2a99c903e8..8c36bcbcf6 100644 --- a/apps/api/pages/api/webhooks/_post.ts +++ b/apps/api/pages/api/webhooks/_post.ts @@ -49,6 +49,9 @@ import { schemaWebhookCreateBodyParams, schemaWebhookReadPublic } from "~/lib/va * eventTypeId: * type: number * description: The event type ID if this webhook should be associated with only that event type + * secret: + * type: string + * description: The secret to verify the authenticity of the received payload * tags: * - webhooks * externalDocs: From 3679854c4340441d0cb1ae57c3018d573c897fdf Mon Sep 17 00:00:00 2001 From: Greg Pabian <35925521+grzpab@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:54:33 +0200 Subject: [PATCH 035/118] chore: [app dir bootstrapping 3] check nullability in AppListCard (#11980) --- apps/web/components/AppListCard.tsx | 17 ++++++++++------- apps/web/playwright/app-list-card.e2e.ts | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 apps/web/playwright/app-list-card.e2e.ts diff --git a/apps/web/components/AppListCard.tsx b/apps/web/components/AppListCard.tsx index 7252a8ffc6..2f9547fbb7 100644 --- a/apps/web/components/AppListCard.tsx +++ b/apps/web/components/AppListCard.tsx @@ -60,14 +60,18 @@ export default function AppListCard(props: AppListCardProps) { const pathname = usePathname(); useEffect(() => { - if (shouldHighlight && highlight) { - const timer = setTimeout(() => { - setHighlight(false); + if (shouldHighlight && highlight && searchParams !== null && pathname !== null) { + timeoutRef.current = setTimeout(() => { const _searchParams = new URLSearchParams(searchParams); _searchParams.delete("hl"); - router.replace(`${pathname}?${_searchParams.toString()}`); + _searchParams.delete("category"); // this comes from params, not from search params + + setHighlight(false); + + const stringifiedSearchParams = _searchParams.toString(); + + router.replace(`${pathname}${stringifiedSearchParams !== "" ? `?${stringifiedSearchParams}` : ""}`); }, 3000); - timeoutRef.current = timer; } return () => { if (timeoutRef.current) { @@ -75,8 +79,7 @@ export default function AppListCard(props: AppListCardProps) { timeoutRef.current = null; } }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [highlight, pathname, router, searchParams, shouldHighlight]); return (
diff --git a/apps/web/playwright/app-list-card.e2e.ts b/apps/web/playwright/app-list-card.e2e.ts new file mode 100644 index 0000000000..780bf759af --- /dev/null +++ b/apps/web/playwright/app-list-card.e2e.ts @@ -0,0 +1,14 @@ +import { test } from "./lib/fixtures"; + +test.describe("AppListCard", async () => { + test("should remove the highlight from the URL", async ({ page, users }) => { + const user = await users.create({}); + await user.apiLogin(); + + await page.goto("/apps/installed/conferencing?hl=daily-video"); + + await page.waitForLoadState(); + + await page.waitForURL("/apps/installed/conferencing"); + }); +}); From 1de60bcfebf87f6e7c8d950e9d0cfd7286aa5b7d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 23 Oct 2023 18:57:22 +0000 Subject: [PATCH 036/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index b11e9ef8b4..9d7cb5be31 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -268,6 +268,7 @@ "set_availability": "Définissez vos disponibilités", "availability_settings": "Paramètres de disponibilité", "continue_without_calendar": "Continuer sans calendrier", + "continue_with": "Continuer avec {{appName}}", "connect_your_calendar": "Connectez votre calendrier", "connect_your_video_app": "Connectez vos applications vidéo", "connect_your_video_app_instructions": "Connectez vos applications vidéo pour les utiliser sur vos types d'événements.", @@ -599,6 +600,7 @@ "hide_book_a_team_member": "Masquer le bouton Réserver un membre d'équipe", "hide_book_a_team_member_description": "Masquez le bouton Réserver un membre d'équipe de vos pages publiques.", "danger_zone": "Zone de danger", + "account_deletion_cannot_be_undone": "Attention, la suppression de compte est irréversible.", "back": "Retour", "cancel": "Annuler", "cancel_all_remaining": "Annuler tous les événements restants", @@ -688,6 +690,7 @@ "people": "Personnes", "your_email": "Votre adresse e-mail", "change_avatar": "Changer d'avatar", + "upload_avatar": "Télécharger un avatar", "language": "Langue", "timezone": "Fuseau horaire", "first_day_of_week": "Premier jour de la semaine", @@ -778,6 +781,7 @@ "disable_guests": "Désactiver les invités", "disable_guests_description": "Désactivez l'ajout d'invités supplémentaires lors de la réservation.", "private_link": "Générer un lien privé", + "enable_private_url": "Rendre le lien privé", "private_link_label": "Lien privé", "private_link_hint": "Votre lien privé sera régénéré après chaque utilisation", "copy_private_link": "Copier le lien privé", @@ -1276,6 +1280,7 @@ "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.", + "bio": "Bio", "delete_account_modal_title": "Supprimer le compte", "confirm_delete_account_modal": "Voulez-vous vraiment supprimer votre compte {{appName}} ?", "delete_my_account": "Supprimer mon compte", @@ -1879,6 +1884,7 @@ "edit_invite_link": "Modifier les paramètres du lien", "invite_link_copied": "Lien d'invitation copié", "invite_link_deleted": "Lien d'invitation supprimé", + "api_key_deleted": "Clé API supprimée", "invite_link_updated": "Paramètres de lien d'invitation enregistrés", "link_expires_after": "Les liens ont été définis pour expirer après...", "one_day": "1 jour", @@ -2051,5 +2057,18 @@ "no_members_found": "Aucun membre trouvé", "event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.", "availability_schedules": "Horaires de disponibilité", + "unauthorized": "Non autorisé", + "select_account_team": "Sélectionner un compte ou une équipe", + "access_event_type": "Lire, modifier, supprimer vos types d'événements", + "access_availability": "Lire, modifier, supprimer vos disponibilités", + "access_bookings": "Lire, modifier, supprimer vos réservations", + "allow_client_to_do": "Autoriser {{clientName}} à faire cela ?", + "allow": "Autoriser", + "edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}", + "resend_invitation": "Renvoyer l'invitation", + "invitation_resent": "L'invitation a été renvoyée.", + "add_client": "Ajouter un client", + "add_new_client": "Ajouter un nouveau client", + "as_csv": "au format CSV", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } From 154af1367a2183664708c65c30f8c8d9d37a114c Mon Sep 17 00:00:00 2001 From: zomars Date: Mon, 23 Oct 2023 13:15:43 -0700 Subject: [PATCH 037/118] hotfix: unreachable rate limits --- packages/lib/checkRateLimitAndThrowError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/checkRateLimitAndThrowError.ts b/packages/lib/checkRateLimitAndThrowError.ts index 88c43f0230..d510a16181 100644 --- a/packages/lib/checkRateLimitAndThrowError.ts +++ b/packages/lib/checkRateLimitAndThrowError.ts @@ -9,7 +9,7 @@ export async function checkRateLimitAndThrowError({ }: RateLimitHelper) { const { remaining, reset } = await rateLimiter()({ rateLimitingType, identifier }); - if (remaining < 0) { + if (remaining < 1) { const convertToSeconds = (ms: number) => Math.floor(ms / 1000); const secondsToWait = convertToSeconds(reset - Date.now()); throw new TRPCError({ From 4ed15d2755e46c575074a9cb2722c58581e9725f Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:49:41 +0100 Subject: [PATCH 038/118] fix: Event Type header layout issues (fix-headerLayout) (#12047) Co-authored-by: gitstart-calcom Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- packages/ui/components/button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/components/button/Button.tsx b/packages/ui/components/button/Button.tsx index 40f327f1cd..95a8cbc1bb 100644 --- a/packages/ui/components/button/Button.tsx +++ b/packages/ui/components/button/Button.tsx @@ -94,7 +94,7 @@ export const buttonClasses = cva( { variant: "icon", size: "base", - className: "min-h-[36px] min-w-[36px] !p-2", + className: "min-h-[36px] min-w-[36px] !p-2 hover:border-default", }, { variant: "icon", From 19f52429b095b01accc456197ca2ebce0ebc5ab8 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 24 Oct 2023 12:03:33 +0200 Subject: [PATCH 039/118] fix: env.example requesting 24 bytes instead of 32 bytes encryption key (#12043) --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index f6f6188a1b..520ce80b27 100644 --- a/.env.example +++ b/.env.example @@ -87,7 +87,7 @@ CRON_ENABLE_APP_SYNC=false # Application Key for symmetric encryption and decryption # must be 32 bytes for AES256 encryption algorithm -# You can use: `openssl rand -base64 24` to generate one +# You can use: `openssl rand -base64 32` to generate one CALENDSO_ENCRYPTION_KEY= # Intercom Config From 051353e7f15687fc5857810de5cfb2d4adef12b6 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:05:53 +0200 Subject: [PATCH 040/118] fix: booking day wrong in booking list (#12007) Co-authored-by: CarinaWolli --- apps/web/components/booking/BookingListItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index e3f4fa7b22..f600c6e697 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -226,6 +226,7 @@ function BookingListItem(booking: BookingItemProps) { }; const startTime = dayjs(booking.startTime) + .tz(user?.timeZone) .locale(language) .format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); From ee08118ed3d172e8e324de3acb5f3a2ae2ef91a5 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 24 Oct 2023 11:16:11 +0100 Subject: [PATCH 041/118] chore: removed vital.json (#12038) --- apps/web/public/static/locales/ar/vital.json | 13 -------- apps/web/public/static/locales/cs/vital.json | 13 -------- apps/web/public/static/locales/de/vital.json | 13 -------- apps/web/public/static/locales/en/vital.json | 13 -------- apps/web/public/static/locales/es/vital.json | 13 -------- apps/web/public/static/locales/fr/vital.json | 13 -------- apps/web/public/static/locales/he/vital.json | 13 -------- apps/web/public/static/locales/hu/vital.json | 1 - apps/web/public/static/locales/it/vital.json | 13 -------- apps/web/public/static/locales/ja/vital.json | 13 -------- apps/web/public/static/locales/ko/vital.json | 13 -------- apps/web/public/static/locales/nl/vital.json | 13 -------- apps/web/public/static/locales/pl/vital.json | 13 -------- .../public/static/locales/pt-BR/vital.json | 13 -------- apps/web/public/static/locales/pt/vital.json | 13 -------- apps/web/public/static/locales/ro/vital.json | 13 -------- apps/web/public/static/locales/ru/vital.json | 13 -------- apps/web/public/static/locales/sr/vital.json | 13 -------- apps/web/public/static/locales/sv/vital.json | 13 -------- apps/web/public/static/locales/tr/vital.json | 13 -------- apps/web/public/static/locales/uk/vital.json | 13 -------- apps/web/public/static/locales/vi/vital.json | 13 -------- .../public/static/locales/zh-CN/vital.json | 13 -------- .../public/static/locales/zh-TW/vital.json | 13 -------- .../vital/components/AppConfiguration.tsx | 30 +++++++++---------- 25 files changed, 14 insertions(+), 316 deletions(-) delete mode 100644 apps/web/public/static/locales/ar/vital.json delete mode 100644 apps/web/public/static/locales/cs/vital.json delete mode 100644 apps/web/public/static/locales/de/vital.json delete mode 100644 apps/web/public/static/locales/en/vital.json delete mode 100644 apps/web/public/static/locales/es/vital.json delete mode 100644 apps/web/public/static/locales/fr/vital.json delete mode 100644 apps/web/public/static/locales/he/vital.json delete mode 100644 apps/web/public/static/locales/hu/vital.json delete mode 100644 apps/web/public/static/locales/it/vital.json delete mode 100644 apps/web/public/static/locales/ja/vital.json delete mode 100644 apps/web/public/static/locales/ko/vital.json delete mode 100644 apps/web/public/static/locales/nl/vital.json delete mode 100644 apps/web/public/static/locales/pl/vital.json delete mode 100644 apps/web/public/static/locales/pt-BR/vital.json delete mode 100644 apps/web/public/static/locales/pt/vital.json delete mode 100644 apps/web/public/static/locales/ro/vital.json delete mode 100644 apps/web/public/static/locales/ru/vital.json delete mode 100644 apps/web/public/static/locales/sr/vital.json delete mode 100644 apps/web/public/static/locales/sv/vital.json delete mode 100644 apps/web/public/static/locales/tr/vital.json delete mode 100644 apps/web/public/static/locales/uk/vital.json delete mode 100644 apps/web/public/static/locales/vi/vital.json delete mode 100644 apps/web/public/static/locales/zh-CN/vital.json delete mode 100644 apps/web/public/static/locales/zh-TW/vital.json diff --git a/apps/web/public/static/locales/ar/vital.json b/apps/web/public/static/locales/ar/vital.json deleted file mode 100644 index 75c0a57f52..0000000000 --- a/apps/web/public/static/locales/ar/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "متصل باستخدام", - "vital_app_sleep_automation": "أتمتة إعادة الجدولة بناءً على بيانات نومك", - "vital_app_automation_description": "يمكنك تحديد معلمات مختلفة لتشغيل إعادة الجدولة بناءً على مقاييس النوم الخاصة بك.", - "vital_app_parameter": "المعلمات", - "vital_app_trigger": "Trigger عندما يساوي أو يكون أقل من", - "vital_app_save_button": "حفظ التكوين", - "vital_app_total_label": "المجموع (المجموع = نوم حركة العين السريعة + النوم الخفيف + النوم العميق)", - "vital_app_duration_label": "المدة (المدة= نهاية وقت النوم - بداية وقت النوم)", - "vital_app_hours": "ساعة", - "vital_app_save_success": "تم حفظ Vital Configurations بنجاح", - "vital_app_save_error": "حدث خطأ أثناء حفظ Vital Configurations الخاصة بك" -} diff --git a/apps/web/public/static/locales/cs/vital.json b/apps/web/public/static/locales/cs/vital.json deleted file mode 100644 index 7d94769411..0000000000 --- a/apps/web/public/static/locales/cs/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Propojeno s aplikací", - "vital_app_sleep_automation": "Automatizace přeplánování spánku", - "vital_app_automation_description": "Na základě měření délky spánku můžete zvolit různé parametry, které spustí přeplánování.", - "vital_app_parameter": "Parametr", - "vital_app_trigger": "Spustit při hodnotě menší nebo rovné", - "vital_app_save_button": "Uložit konfiguraci", - "vital_app_total_label": "Celková doba (celková doba = REM + lehký spánek + hluboký spánek)", - "vital_app_duration_label": "Délka (délka = konec uložení ke spánku − začátek uložení ke spánku)", - "vital_app_hours": "h", - "vital_app_save_success": "Uložení konfigurace aplikace Vital se zdařilo", - "vital_app_save_error": "Při ukládání konfigurace aplikace Vital se vyskytla chyba" -} diff --git a/apps/web/public/static/locales/de/vital.json b/apps/web/public/static/locales/de/vital.json deleted file mode 100644 index 02f8804983..0000000000 --- a/apps/web/public/static/locales/de/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Verbunden mit", - "vital_app_sleep_automation": "Schlaf Terminumbuchunsautomatisierung", - "vital_app_automation_description": "Sie können verschiedene Parameter auswählen, um die Umbuchung basierend auf Ihren Schlafmetriken auszulösen.", - "vital_app_parameter": "Parameter", - "vital_app_trigger": "Auslöser kleiner oder gleich", - "vital_app_save_button": "Einstellungen speichern", - "vital_app_total_label": "Gesamt (Gesamt= rem + leichter Schlaf + tiefer Schlaf)", - "vital_app_duration_label": "Dauer (Dauer = Schlafzeitende - Schlafzeit start)", - "vital_app_hours": "Stunden", - "vital_app_save_success": "Erfolgreich Ihre Vital-Konfigurationen wurden Erfolgreich gespeichert", - "vital_app_save_error": "Ein Fehler ist aufgetreten beim Speichern Ihrer Vital Einstellungen" - } diff --git a/apps/web/public/static/locales/en/vital.json b/apps/web/public/static/locales/en/vital.json deleted file mode 100644 index a08a9058b2..0000000000 --- a/apps/web/public/static/locales/en/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Connected with", - "vital_app_sleep_automation": "Sleeping reschedule automation", - "vital_app_automation_description": "You can select different parameters to trigger the reschedule based on your sleeping metrics.", - "vital_app_parameter": "Parameter", - "vital_app_trigger": "Trigger at below or equal than", - "vital_app_save_button": "Save configuration", - "vital_app_total_label": "Total (total = rem + light sleep + deep sleep)", - "vital_app_duration_label": "Duration (duration = bedtime end - bedtime start)", - "vital_app_hours": "hours", - "vital_app_save_success": "Success saving your Vital Configurations", - "vital_app_save_error": "An error ocurred saving your Vital Configurations" -} diff --git a/apps/web/public/static/locales/es/vital.json b/apps/web/public/static/locales/es/vital.json deleted file mode 100644 index 64c6324058..0000000000 --- a/apps/web/public/static/locales/es/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Conectado con", - "vital_app_sleep_automation": "Automatización de reprogramación del sueño", - "vital_app_automation_description": "Puede seleccionar diferentes parámetros para activar la reprogramación automática del sueño.", - "vital_app_parameter": "Parámetro", - "vital_app_trigger": "Activar cuando sea igual o menor que", - "vital_app_save_button": "Guardar configuración", - "vital_app_total_label": "Total (total = rem + sueño ligero + sueño profundo)", - "vital_app_duration_label": "Duración (duración = horario que se levanta de la cama - horario que se acuesta en la cama)", - "vital_app_hours": "horas", - "vital_app_save_success": "Guardado exitoso de sus configuraciones de Vital App ", - "vital_app_save_error": "Ocurrió un error al intentar guardar sus configuraciones de Vital App " -} diff --git a/apps/web/public/static/locales/fr/vital.json b/apps/web/public/static/locales/fr/vital.json deleted file mode 100644 index 13df196a1b..0000000000 --- a/apps/web/public/static/locales/fr/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Connecté avec", - "vital_app_sleep_automation": "Automatisation de la reprogrammation du sommeil", - "vital_app_automation_description": "Vous pouvez sélectionner différents paramètres pour déclencher la reprogrammation en fonction de vos paramètres de sommeil.", - "vital_app_parameter": "Paramètre", - "vital_app_trigger": "Trigger inférieur ou égal à", - "vital_app_save_button": "Enregistrer la configuration", - "vital_app_total_label": "Total (total = rem + sommeil léger + sommeil profond)", - "vital_app_duration_label": "Durée (durée = heure de fin du coucher - début du sommeil)", - "vital_app_hours": "heures", - "vital_app_save_success": "Enregistrement de vos configurations Vital réussi", - "vital_app_save_error": "Une erreur est survenu lors de l'enregistrement de vos configurations Vital" -} diff --git a/apps/web/public/static/locales/he/vital.json b/apps/web/public/static/locales/he/vital.json deleted file mode 100644 index 5fd291eb48..0000000000 --- a/apps/web/public/static/locales/he/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "מחובר/ת דרך", - "vital_app_sleep_automation": "תזמון מחדש באופן אוטומטי על סמך נתוני השינה שלך", - "vital_app_automation_description": "ניתן לבחור פרמטרים שונים כדי להפעיל את קביעת המועד מחדש על סמך מדדי השינה שלך.", - "vital_app_parameter": "פרמטר", - "vital_app_trigger": "להפעיל כאשר הערך הוא פחות מ- או שווה ל-", - "vital_app_save_button": "שמירת התצורה", - "vital_app_total_label": "סה\"כ (סה\"כ = שנת REM + שינה קלה + שינה עמוקה)", - "vital_app_duration_label": "משך זמן (משך זמן = שעת סיום זמן שינה פחות שעת תחילת זמן שינה)", - "vital_app_hours": "שעות", - "vital_app_save_success": "שמירת ה-Vital Configurations שלך בוצעה בהצלחה", - "vital_app_save_error": "אירעה שגיאה במהלך שמירת ה-Vital Configurations שלך" -} diff --git a/apps/web/public/static/locales/hu/vital.json b/apps/web/public/static/locales/hu/vital.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/apps/web/public/static/locales/hu/vital.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/web/public/static/locales/it/vital.json b/apps/web/public/static/locales/it/vital.json deleted file mode 100644 index e5a21b7bbb..0000000000 --- a/apps/web/public/static/locales/it/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Connesso con", - "vital_app_sleep_automation": "Automazione della riprogrammazione in base ai dati del tuo sonno", - "vital_app_automation_description": "Puoi scegliere vari parametri per attivare la riprogrammazione in base ai parametri del tuo sonno.", - "vital_app_parameter": "Parametro", - "vital_app_trigger": "Attiva se inferiore o uguale a", - "vital_app_save_button": "Salva configurazione", - "vital_app_total_label": "Totale (totale = REM + sonno leggero + sonno profondo)", - "vital_app_duration_label": "Durata (durata = fine del sonno - inizio del sonno)", - "vital_app_hours": "ore", - "vital_app_save_success": "Configurazioni di Vital salvate correttamente", - "vital_app_save_error": "Si è verificato un errore durante il salvataggio delle configurazioni di Vital" -} diff --git a/apps/web/public/static/locales/ja/vital.json b/apps/web/public/static/locales/ja/vital.json deleted file mode 100644 index 9c281a1ada..0000000000 --- a/apps/web/public/static/locales/ja/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "接続先", - "vital_app_sleep_automation": "Sleeping リスケジュールの自動設定", - "vital_app_automation_description": "リスケジュールのトリガーとなるパラメータは、スリーピングメトリクスに基づいてさまざまに選択できます。", - "vital_app_parameter": "パラメータ", - "vital_app_trigger": "以下でトリガー", - "vital_app_save_button": "設定を保存", - "vital_app_total_label": "合計(合計 = レム + 軽い睡眠 + 深い睡眠)", - "vital_app_duration_label": "持続時間(持続時間 = 起床時間 - 就寝時間)", - "vital_app_hours": "時間", - "vital_app_save_success": "Vital 設定の保存に成功しました", - "vital_app_save_error": "Vital 設定の保存中にエラーが発生しました" -} diff --git a/apps/web/public/static/locales/ko/vital.json b/apps/web/public/static/locales/ko/vital.json deleted file mode 100644 index b70b7081d9..0000000000 --- a/apps/web/public/static/locales/ko/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "연결된 대상", - "vital_app_sleep_automation": "수면 재예약 자동화", - "vital_app_automation_description": "수면 지표에 따라 다른 매개변수를 선택하여 일정을 변경할 수 있습니다.", - "vital_app_parameter": "매개변수", - "vital_app_trigger": "같거나 미만인 범위에서 트리거", - "vital_app_save_button": "구성 저장", - "vital_app_total_label": "총계(총계 = 렘 + 가벼운 수면 + 깊은 수면)", - "vital_app_duration_label": "지속 시간(지속 시간 = 취침 시간 끝 - 취침 시간 시작)", - "vital_app_hours": "시간", - "vital_app_save_success": "주요 구성을 저장함", - "vital_app_save_error": "주요 구성을 저장하는 중 오류가 발생했습니다" -} diff --git a/apps/web/public/static/locales/nl/vital.json b/apps/web/public/static/locales/nl/vital.json deleted file mode 100644 index 71d61357cf..0000000000 --- a/apps/web/public/static/locales/nl/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Verbonden met", - "vital_app_sleep_automation": "Automatisering opnieuw plannen van slaap", - "vital_app_automation_description": "U kunt verschillende parameters selecteren om het opnieuw plannen te activeren, op basis van uw slaapmetriek.", - "vital_app_parameter": "Parameter", - "vital_app_trigger": "Activatie bij minder dan of gelijk aan", - "vital_app_save_button": "Configuratie opslaan", - "vital_app_total_label": "Totaal (totaal = remslaap + lichte slaap + diepe slaap)", - "vital_app_duration_label": "Duur (duur = einde bedtijd - start bedtijd)", - "vital_app_hours": "uur", - "vital_app_save_success": "Uw Vital-configuraties zijn opgeslagen", - "vital_app_save_error": "Er is een fout opgetreden bij het opslaan van uw Vital-configuraties" -} diff --git a/apps/web/public/static/locales/pl/vital.json b/apps/web/public/static/locales/pl/vital.json deleted file mode 100644 index 82e5829319..0000000000 --- a/apps/web/public/static/locales/pl/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Połączono z", - "vital_app_sleep_automation": "Automatyczna zmiana harmonogramu snu", - "vital_app_automation_description": "Możesz wybrać różne parametry, które spowodują zmianę harmonogramu w zależności od jakości Twojego snu.", - "vital_app_parameter": "Parametr", - "vital_app_trigger": "Wyzwalaj przy wartości mniejszej lub równej", - "vital_app_save_button": "Zapisz konfigurację", - "vital_app_total_label": "Łącznie (łącznie = REM + płytki sen + głęboki sen)", - "vital_app_duration_label": "Czas trwania (czas trwania = koniec snu - początek snu)", - "vital_app_hours": "godz.", - "vital_app_save_success": "Pomyślnie zapisano Twoje Vital Configurations", - "vital_app_save_error": "Podczas zapisywania ustawień parametrów życiowych wystąpił błąd" -} diff --git a/apps/web/public/static/locales/pt-BR/vital.json b/apps/web/public/static/locales/pt-BR/vital.json deleted file mode 100644 index 2c36117865..0000000000 --- a/apps/web/public/static/locales/pt-BR/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Conectou-se com", - "vital_app_sleep_automation": "Automação de reagendamento de sono", - "vital_app_automation_description": "Você pode selecionar parâmetros diferentes para acionar o reagendamento conforme suas métricas de sono.", - "vital_app_parameter": "Parâmetro", - "vital_app_trigger": "Aciona quando for menor ou igual a", - "vital_app_save_button": "Salvar configuração", - "vital_app_total_label": "Total (total = rem + sono leve + sono profundo)", - "vital_app_duration_label": "Duração (duração = fim do horário de dormir - início do horário de dormir)", - "vital_app_hours": "horas", - "vital_app_save_success": "Sucesso ao salvar suas configurações do Vital", - "vital_app_save_error": "Ocorreu um erro ao salvar suas configurações do Vital" -} diff --git a/apps/web/public/static/locales/pt/vital.json b/apps/web/public/static/locales/pt/vital.json deleted file mode 100644 index dfcc798f5b..0000000000 --- a/apps/web/public/static/locales/pt/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Ligado a", - "vital_app_sleep_automation": "Reagendar automaticamente durante o sono", - "vital_app_automation_description": "Pode seleccionar diferentes parâmetros para accionar o reagendamento com base nas suas métricas de sono.", - "vital_app_parameter": "Parâmetro", - "vital_app_trigger": "Executar abaixo ou igual a", - "vital_app_save_button": "Guardar configuração", - "vital_app_total_label": "Total (total = rem + sono leve + sono profundo)", - "vital_app_duration_label": "Duração (duração = fim do sono - início do sono)", - "vital_app_hours": "horas", - "vital_app_save_success": "As suas configurações vitais foram guardadas com sucesso", - "vital_app_save_error": "Ocorreu um erro ao guardar as suas configurações vitais" -} diff --git a/apps/web/public/static/locales/ro/vital.json b/apps/web/public/static/locales/ro/vital.json deleted file mode 100644 index 3b22229949..0000000000 --- a/apps/web/public/static/locales/ro/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Conectat cu", - "vital_app_sleep_automation": "Automatizare reprogramare somn", - "vital_app_automation_description": "Puteți selecta diferiți parametri pentru a declanșa reprogramarea în funcție de valorile dvs. legate de somn.", - "vital_app_parameter": "Parametru", - "vital_app_trigger": "Declanșare la valori de sau mai mici de", - "vital_app_save_button": "Salvare configurație", - "vital_app_total_label": "Total (total = rem + somn ușor + somn profund)", - "vital_app_duration_label": "Durată (durata = finalul orei de somn - începutul orei de somn)", - "vital_app_hours": "ore", - "vital_app_save_success": "Configurațiile dvs. pentru semnele vitale au fost salvate cu succes", - "vital_app_save_error": "A intervenit o eroare la salvarea configurațiilor dvs. pentru semnele vitale" -} diff --git a/apps/web/public/static/locales/ru/vital.json b/apps/web/public/static/locales/ru/vital.json deleted file mode 100644 index c7c04b11cf..0000000000 --- a/apps/web/public/static/locales/ru/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Соединено с аккаунтом", - "vital_app_sleep_automation": "Автоматический перенос с учетом режима сна", - "vital_app_automation_description": "Вы можете настроить параметры переноса событий с учетом ваших показателей сна.", - "vital_app_parameter": "Параметр", - "vital_app_trigger": "Запускать, если меньше или равно", - "vital_app_save_button": "Сохранить конфигурацию", - "vital_app_total_label": "Всего (всего = быстрый сон + легкий сон + глубокий сон)", - "vital_app_duration_label": "Продолжительность (продолжительность = время окончания сна – время начала сна)", - "vital_app_hours": "ч.", - "vital_app_save_success": "Конфигурации Vital сохранены", - "vital_app_save_error": "Ошибка при сохранении конфигураций Vital" -} diff --git a/apps/web/public/static/locales/sr/vital.json b/apps/web/public/static/locales/sr/vital.json deleted file mode 100644 index 5bec384765..0000000000 --- a/apps/web/public/static/locales/sr/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Povezan sa", - "vital_app_sleep_automation": "Automatizacija promene termina na osnovu parametara spavanja", - "vital_app_automation_description": "Možete izabrati različite parametre koji će aktivirati promenu termina na osnovu vaših parametara spavanja.", - "vital_app_parameter": "Parametar", - "vital_app_trigger": "Aktiviraj kada je jednako ili manje od", - "vital_app_save_button": "Sačuvaj konfiguraciju", - "vital_app_total_label": "Ukupno (ukupno = REM + lagani san + dubok san)", - "vital_app_duration_label": "Trajanje (trajanje = kraj spavanja - početak spavanja)", - "vital_app_hours": "sati", - "vital_app_save_success": "Vaše konfiguracije vitalnih znakova su uspešno sačuvane", - "vital_app_save_error": "Došlo je do greške pri čuvanju vaših konfiguracija vitalnih znakova" -} diff --git a/apps/web/public/static/locales/sv/vital.json b/apps/web/public/static/locales/sv/vital.json deleted file mode 100644 index 4c3cd550fe..0000000000 --- a/apps/web/public/static/locales/sv/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Ansluten till", - "vital_app_sleep_automation": "Automatisering för att schemalägga sömn på nytt", - "vital_app_automation_description": "Du kan välja olika parametrar för att utlösa tidsplanen baserat på dina sömnmått.", - "vital_app_parameter": "Parameter", - "vital_app_trigger": "Utlösare under eller lika med", - "vital_app_save_button": "Spara konfiguration", - "vital_app_total_label": "Totalt (totalt = rem + lätt sömn + djupsömn)", - "vital_app_duration_label": "Varaktighet (varaktighet = sängdags slut - sängdags start)", - "vital_app_hours": "timmar", - "vital_app_save_success": "Dina grundläggande konfigurationer sparades", - "vital_app_save_error": "Det gick inte att spara dina grundläggande konfigurationer" -} \ No newline at end of file diff --git a/apps/web/public/static/locales/tr/vital.json b/apps/web/public/static/locales/tr/vital.json deleted file mode 100644 index 8fd47532e3..0000000000 --- a/apps/web/public/static/locales/tr/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Şununla bağlan:", - "vital_app_sleep_automation": "Uykuyu yeniden programlama otomasyonu", - "vital_app_automation_description": "Uyku değerlerinize göre yeniden programlamayı tetiklemek için farklı parametreler seçebilirsiniz.", - "vital_app_parameter": "Parametre", - "vital_app_trigger": "Şu değerin altında veya eşit olduğunda tetikle:", - "vital_app_save_button": "Yapılandırmayı kaydet", - "vital_app_total_label": "Toplam (toplam = REM + hafif uyku + derin uyku)", - "vital_app_duration_label": "Süre (süre = uyku saatinin bitişi - uyku saatinin başlangıcı)", - "vital_app_hours": "saat", - "vital_app_save_success": "Önemli Yapılandırmalarınız başarıyla kaydedildi", - "vital_app_save_error": "Önemli Yapılandırmalarınız kaydedilirken bir hata oluştu" -} diff --git a/apps/web/public/static/locales/uk/vital.json b/apps/web/public/static/locales/uk/vital.json deleted file mode 100644 index 50b7d556cf..0000000000 --- a/apps/web/public/static/locales/uk/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Підключено до", - "vital_app_sleep_automation": "Автоматизація переналаштування розкладу", - "vital_app_automation_description": "Ви можете вибирати різні параметри, щоб виконувати переналаштування розкладу на основі показників сну.", - "vital_app_parameter": "Параметр", - "vital_app_trigger": "Ініціювати, якщо показник не перевищує", - "vital_app_save_button": "Зберегти конфігурацію", - "vital_app_total_label": "Усього (усього = швидкий сон + поверхневий сон + глибокий сон)", - "vital_app_duration_label": "Тривалість (тривалість = час, коли ви встали з ліжка - час, коли ви лягли в ліжко)", - "vital_app_hours": "год", - "vital_app_save_success": "Ваші показники Vital Configurations збережено", - "vital_app_save_error": "Сталася помилка під час збереження ваших показників Vital Configurations" -} diff --git a/apps/web/public/static/locales/vi/vital.json b/apps/web/public/static/locales/vi/vital.json deleted file mode 100644 index 60fc345ebf..0000000000 --- a/apps/web/public/static/locales/vi/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "Đã kết nối với", - "vital_app_sleep_automation": "Tự động sắp lại lịch ngủ", - "vital_app_automation_description": "Bạn có thể chọn những tham số khác nhau để kích hoạt sắp lịch lại dựa trên số liệu thống kê giấc ngủ.", - "vital_app_parameter": "Tham số", - "vital_app_trigger": "Trigger ở mức dưới hoặc bằng", - "vital_app_save_button": "Lưu cấu hình", - "vital_app_total_label": "Tổng (tổng = giấc ngủ rem + ngủ nông + ngủ sâu)", - "vital_app_duration_label": "Thời lượng (thời lượng = giờ dậy - giờ bắt đầu ngủ)", - "vital_app_hours": "giờ", - "vital_app_save_success": "Lưu thành công cấu hình Vital của bạn", - "vital_app_save_error": "Có lỗi xảy ra khi lưu cấu hình Vital" -} diff --git a/apps/web/public/static/locales/zh-CN/vital.json b/apps/web/public/static/locales/zh-CN/vital.json deleted file mode 100644 index 124ac24a84..0000000000 --- a/apps/web/public/static/locales/zh-CN/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "已连接到", - "vital_app_sleep_automation": "睡眠重新安排自动化", - "vital_app_automation_description": "您可以选择不同的参数以根据您的睡眠指标触发重新安排。", - "vital_app_parameter": "参数", - "vital_app_trigger": "低于或等于时触发", - "vital_app_save_button": "保存配置", - "vital_app_total_label": "总合(总合 = 快速眼动睡眠 + 轻度睡眠 + 深度睡眠)", - "vital_app_duration_label": "时长(时长 = 就寝结束时间 - 就寝开始时间)", - "vital_app_hours": "小时", - "vital_app_save_success": "成功保存您的 Vital 配置", - "vital_app_save_error": "保存您的 Vital 配置时出错" -} diff --git a/apps/web/public/static/locales/zh-TW/vital.json b/apps/web/public/static/locales/zh-TW/vital.json deleted file mode 100644 index e134a03e3f..0000000000 --- a/apps/web/public/static/locales/zh-TW/vital.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "connected_vital_app": "已連至", - "vital_app_sleep_automation": "睡眠重新預定自動化", - "vital_app_automation_description": "您可以選取不同的參數,以根據您的睡眠指標來觸發重新預定。", - "vital_app_parameter": "參數", - "vital_app_trigger": "低於下列值或相等時觸發", - "vital_app_save_button": "儲存設定", - "vital_app_total_label": "總計 (總計 = 快速動眼期 + 淺層睡眠 + 深層睡眠)", - "vital_app_duration_label": "時間長度 (時間長度 = 起床時間 - 上床時間)", - "vital_app_hours": "小時", - "vital_app_save_success": "成功儲存您的 Vital 設定", - "vital_app_save_error": "儲存您的 Vital 設定時發生錯誤" -} diff --git a/packages/app-store/vital/components/AppConfiguration.tsx b/packages/app-store/vital/components/AppConfiguration.tsx index fde7cdd3b0..6e20585f1f 100644 --- a/packages/app-store/vital/components/AppConfiguration.tsx +++ b/packages/app-store/vital/components/AppConfiguration.tsx @@ -1,5 +1,4 @@ import { useEffect, useState, useMemo } from "react"; -import { useTranslation } from "react-i18next"; import { Button, Select, showToast } from "@calcom/ui"; @@ -34,15 +33,14 @@ const saveSettings = async ({ }; const AppConfiguration = (props: IAppConfigurationProps) => { - const { t } = useTranslation(); const [credentialId] = props.credentialIds; const options = useMemo( () => [ - { label: t("vital_app_total_label", { ns: "vital" }), value: "total" }, - { label: t("vital_app_duration_label", { ns: "vital" }), value: "duration" }, + { label: "Total (total = rem + light sleep + deep sleep)", value: "total" }, + { label: "Duration (duration = bedtime end - bedtime start)", value: "duration" }, ], - [t] + [] ); const [selectedParam, setSelectedParam] = useState<{ label: string; value: string }>(options[0]); @@ -92,21 +90,21 @@ const AppConfiguration = (props: IAppConfigurationProps) => { return (

- - {t("connected_vital_app", { ns: "vital" })} Vital App: {connected ? "Yes" : "No"} - + Connected with Vital App: {connected ? "Yes" : "No"}


- {t("vital_app_sleep_automation", { ns: "vital" })} + Sleeping reschedule automation +

+

+ You can select different parameters to trigger the reschedule based on your sleeping metrics.

-

{t("vital_app_automation_description", { ns: "vital" })}

@@ -125,7 +123,7 @@ const AppConfiguration = (props: IAppConfigurationProps) => {
@@ -142,7 +140,7 @@ const AppConfiguration = (props: IAppConfigurationProps) => { className="pr-12shadow-sm border-default mt-1 block w-full rounded-sm border py-2 pl-6 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" />

- {t("vital_app_hours", { ns: "vital" })} + hours

@@ -154,9 +152,9 @@ const AppConfiguration = (props: IAppConfigurationProps) => { try { setSaveLoading(true); await saveSettings({ parameter: selectedParam, sleepValue: sleepValue }); - showToast(t("vital_app_save_success"), "success"); + showToast("Success saving your Vital Configurations", "success"); } catch (error) { - showToast(t("vital_app_save_error"), "error"); + showToast("An error ocurred saving your Vital Configurations", "error"); setSaveLoading(false); } setTouchedForm(false); @@ -164,7 +162,7 @@ const AppConfiguration = (props: IAppConfigurationProps) => { }} loading={saveLoading} disabled={disabledSaveButton}> - {t("vital_app_save_button", { ns: "vital" })} + Save configuration
From 96810b5ba12a11ceb6ebb695dc1fd07e461a96dd Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 07:22:52 -0300 Subject: [PATCH 042/118] test: Create E2E tests for bookings with custom/required Long Text + other questions (teste2e-longTextQuestion) (#11559) * Add E2E tests for long test question in a regular booking * Remove unnecessary changes * change all tests * Update longTextQuestion.e2e.ts * refactor * Update longTextQuestion.e2e.ts * refactor: split cancelAndRescheduleBooking --------- Co-authored-by: gitstart-calcom Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: Keith Williams Co-authored-by: Morgan Vernay --- .../booking/longTextQuestion.e2e.ts | 483 ++++++++++++++++++ .../playwright/booking/phoneQuestion.e2e.ts | 120 +++-- .../playwright/fixtures/regularBookings.ts | 19 +- 3 files changed, 585 insertions(+), 37 deletions(-) create mode 100644 apps/web/playwright/booking/longTextQuestion.e2e.ts diff --git a/apps/web/playwright/booking/longTextQuestion.e2e.ts b/apps/web/playwright/booking/longTextQuestion.e2e.ts new file mode 100644 index 0000000000..3f7818bddd --- /dev/null +++ b/apps/web/playwright/booking/longTextQuestion.e2e.ts @@ -0,0 +1,483 @@ +import { loginUser } from "../fixtures/regularBookings"; +import { test } from "../lib/fixtures"; + +test.describe("Booking With Long Text Question and Each Other Question", () => { + const bookingOptions = { hasPlaceholder: true, isRequired: true }; + + test.beforeEach(async ({ page, users }) => { + await loginUser(users); + await page.goto("/event-types"); + }); + + test("Long Text and Address required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("address", "address-test", "address test", true, "address test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Address question (both required)", + secondQuestion: "address", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Address not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("address", "address-test", "address test", false, "address test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Address question (only Long Text required)", + secondQuestion: "address", + options: { ...bookingOptions, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test.describe("Booking With Long Text Question and Checkbox Group Question", () => { + test("Long Text and Checkbox Group required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Checkbox Group question (both required)", + secondQuestion: "checkbox", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Checkbox Group not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Checkbox Group question (only Long Text required)", + secondQuestion: "checkbox", + options: { ...bookingOptions, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and checkbox Question", () => { + test("Long Text and checkbox required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Checkbox question (only Long Text required)", + secondQuestion: "boolean", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and checkbox not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Checkbox question (only Long Text required)", + secondQuestion: "boolean", + options: { ...bookingOptions, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and Multiple email Question", () => { + const bookingOptions = { hasPlaceholder: true, isRequired: true }; + test("Long Text and Multiple email required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion( + "multiemail", + "multiemail-test", + "multiemail test", + true, + "multiemail test" + ); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Multiple email question (both required)", + secondQuestion: "multiemail", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Multiple email not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion( + "multiemail", + "multiemail-test", + "multiemail test", + false, + "multiemail test" + ); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Multiple email question (only Long Text required)", + secondQuestion: "multiemail", + options: { hasPlaceholder: true, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and multiselect Question", () => { + test("Long Text and multiselect text required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and multiselect question (both required)", + secondQuestion: "multiselect", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and multiselect text not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and multiselect question (only long text required)", + secondQuestion: "multiselect", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and Number Question", () => { + test("Long Text and Number required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Number question (both required)", + secondQuestion: "multiselect", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test("Long Text required and Number not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Number question (only Long Textß required)", + secondQuestion: "multiselect", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test.describe("Booking With Long Text Question and Phone Question", () => { + test("Long Text and Phone required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Phone question (both required)", + secondQuestion: "phone", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Phone not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Phone question (only Long Text required)", + secondQuestion: "phone", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and Radio group Question", () => { + test("Long Text and Radio group required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("radio", "radio-test", "radio test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Radio Group question (both required)", + secondQuestion: "radio", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Radio group not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("radio", "radio-test", "radio test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Radio Group question (only Long Text required)", + secondQuestion: "radio", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and select Question", () => { + test("Long Text and select required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("select", "select-test", "select test", true); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Select question (both required)", + secondQuestion: "select", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and select not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("select", "select-test", "select test", false); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Select question (only Long Text required)", + secondQuestion: "select", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); + + test.describe("Booking With Long Text Question and Short text question", () => { + const bookingOptions = { hasPlaceholder: true, isRequired: true }; + test("Long Text and Short text required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("text", "text-test", "text test", true, "text test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Text question (both required)", + secondQuestion: "text", + options: bookingOptions, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + + test("Long Text required and Short text not required", async ({ bookingPage }) => { + await bookingPage.goToEventType("30 min"); + await bookingPage.goToTab("event_advanced_tab_title"); + await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test"); + await bookingPage.addQuestion("text", "text-test", "text test", false, "text test"); + await bookingPage.updateEventType(); + const eventTypePage = await bookingPage.previewEventType(); + await bookingPage.selectTimeSlot(eventTypePage); + await bookingPage.fillAndConfirmBooking({ + eventTypePage, + placeholderText: "Please share anything that will help prepare for our meeting.", + question: "textarea", + fillText: "Test Long Text question and Text question (only Long Text required)", + secondQuestion: "text", + options: { hasPlaceholder: false, isRequired: false }, + }); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); + }); + }); +}); diff --git a/apps/web/playwright/booking/phoneQuestion.e2e.ts b/apps/web/playwright/booking/phoneQuestion.e2e.ts index f8236c34ff..481b489cbc 100644 --- a/apps/web/playwright/booking/phoneQuestion.e2e.ts +++ b/apps/web/playwright/booking/phoneQuestion.e2e.ts @@ -26,10 +26,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "address", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Address not required", async ({ bookingPage }) => { + test("Phone required and Address not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("address", "address-test", "address test", false, "address test"); await bookingPage.updateEventType(); @@ -43,7 +46,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "address", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); test.describe("Booking With Phone Question and checkbox group Question", () => { @@ -62,10 +68,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "checkbox", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and checkbox group not required", async ({ bookingPage }) => { + test("Phone required and checkbox group not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false); await bookingPage.updateEventType(); @@ -79,7 +88,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "checkbox", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -98,9 +110,12 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "boolean", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and checkbox not required", async ({ bookingPage }) => { + test("Phone required and checkbox not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false); await bookingPage.updateEventType(); @@ -114,7 +129,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "boolean", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -133,10 +151,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "textarea", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Long text not required", async ({ bookingPage }) => { + test("Phone required and Long text not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test"); await bookingPage.updateEventType(); @@ -150,7 +171,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "textarea", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -176,10 +200,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "multiemail", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Multi email not required", async ({ bookingPage }) => { + test("Phone required and Multi email not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion( "multiemail", @@ -199,7 +226,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "multiemail", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -218,10 +248,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "multiselect", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and multiselect text not required", async ({ bookingPage }) => { + test("Phone required and multiselect text not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false); await bookingPage.updateEventType(); @@ -235,7 +268,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "multiselect", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -254,10 +290,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "number", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Number not required", async ({ bookingPage }) => { + test("Phone required and Number not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("number", "number-test", "number test", false, "number test"); await bookingPage.updateEventType(); @@ -271,7 +310,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "number", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -290,10 +332,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "radio", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Radio group not required", async ({ bookingPage }) => { + test("Phone required and Radio group not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("radio", "radio-test", "radio test", false); await bookingPage.updateEventType(); @@ -307,7 +352,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "radio", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -326,10 +374,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "select", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and select not required", async ({ bookingPage }) => { + test("Phone required and select not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("select", "select-test", "select test", false, "select test"); await bookingPage.updateEventType(); @@ -343,7 +394,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "select", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); @@ -363,10 +417,13 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "text", options: bookingOptions, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); - test("Phone and Short text not required", async ({ bookingPage }) => { + test("Phone required and Short text not required", async ({ bookingPage }) => { await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test"); await bookingPage.addQuestion("text", "text-test", "text test", false, "text test"); await bookingPage.updateEventType(); @@ -380,7 +437,10 @@ test.describe("Booking With Phone Question and Each Other Question", () => { secondQuestion: "text", options: { ...bookingOptions, isRequired: false }, }); - await bookingPage.cancelAndRescheduleBooking(eventTypePage); + await bookingPage.rescheduleBooking(eventTypePage); + await bookingPage.assertBookingRescheduled(eventTypePage); + await bookingPage.cancelBooking(eventTypePage); + await bookingPage.assertBookingCanceled(eventTypePage); }); }); }); diff --git a/apps/web/playwright/fixtures/regularBookings.ts b/apps/web/playwright/fixtures/regularBookings.ts index 447debd83a..3ad4c0e7d3 100644 --- a/apps/web/playwright/fixtures/regularBookings.ts +++ b/apps/web/playwright/fixtures/regularBookings.ts @@ -174,18 +174,17 @@ export function createBookingPageFixture(page: Page) { await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule"); await page.getByTestId("confirm-reschedule-button").click(); }, - verifyReschedulingSuccess: async () => { - await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible(); - }, - cancelBookingWithReason: async () => { + + cancelBookingWithReason: async (page: Page) => { await page.getByTestId("cancel").click(); await page.getByTestId("cancel_reason").fill("Test cancel"); await page.getByTestId("confirm_cancel").click(); }, - verifyBookingCancellation: async () => { + assertBookingCanceled: async (page: Page) => { await expect(page.getByTestId("cancelled-headline")).toBeVisible(); }, - cancelAndRescheduleBooking: async (eventTypePage: Page) => { + + rescheduleBooking: async (eventTypePage: Page) => { await eventTypePage.getByText("Reschedule").click(); while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) { await eventTypePage.getByRole("button", { name: "View next" }).click(); @@ -194,7 +193,13 @@ export function createBookingPageFixture(page: Page) { await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click(); await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule"); await eventTypePage.getByTestId("confirm-reschedule-button").click(); - await expect(eventTypePage.getByText(scheduleSuccessfullyText)).toBeVisible(); + }, + + assertBookingRescheduled: async (page: Page) => { + await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible(); + }, + + cancelBooking: async (eventTypePage: Page) => { await eventTypePage.getByTestId("cancel").click(); await eventTypePage.getByTestId("cancel_reason").fill("Test cancel"); await eventTypePage.getByTestId("confirm_cancel").click(); From b934c74c30bb77bd7a5015b9357c01ef74af664c Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 24 Oct 2023 16:12:36 +0530 Subject: [PATCH 043/118] fix: Avatar slug and cal links for cross org users (#12031) --- .../steps-views/UserProfile.tsx | 18 +++--- apps/web/components/team/screens/Team.tsx | 14 +++-- apps/web/components/ui/avatar/UserAvatar.tsx | 19 ++++++ .../components/ui/avatar/UserAvatarGroup.tsx | 20 ++++++ .../ui/avatar/UserAvatarGroupWithOrg.tsx | 30 +++++++++ apps/web/pages/[user].tsx | 42 ++++++++++--- apps/web/pages/api/user/avatar.ts | 50 +++++++-------- .../web/pages/settings/my-account/profile.tsx | 22 +++++-- apps/web/pages/signup.tsx | 4 +- apps/web/pages/team/[slug].tsx | 25 +++----- .../features/auth/lib/next-auth-options.ts | 4 +- .../components/event-meta/Members.tsx | 63 +++++-------------- .../components/OrganizationAvatar.tsx | 32 ---------- .../components/OrganizationMemberAvatar.tsx | 47 ++++++++++++++ .../ee/organizations/lib/orgDomains.ts | 34 +++++++++- .../ee/teams/components/MemberListItem.tsx | 10 +-- .../ee/teams/pages/team-profile-view.tsx | 4 +- .../features/ee/users/server/trpc-router.ts | 3 + .../components/ChildrenEventTypeSelect.tsx | 4 +- .../features/eventtypes/lib/getPublicEvent.ts | 12 ++-- packages/features/shell/Shell.tsx | 4 +- .../components/AvailabilitySliderTable.tsx | 16 ++++- packages/lib/getAvatarUrl.ts | 24 +++++++ packages/lib/getEventTypeById.ts | 6 +- packages/lib/server/getBrand.ts | 4 +- packages/lib/server/queries/teams/index.ts | 9 ++- .../routers/loggedInViewer/me.handler.ts | 7 +-- .../team/listTeamAvailability.handler.ts | 6 ++ 28 files changed, 351 insertions(+), 182 deletions(-) create mode 100644 apps/web/components/ui/avatar/UserAvatar.tsx create mode 100644 apps/web/components/ui/avatar/UserAvatarGroup.tsx create mode 100644 apps/web/components/ui/avatar/UserAvatarGroupWithOrg.tsx delete mode 100644 packages/features/ee/organizations/components/OrganizationAvatar.tsx create mode 100644 packages/features/ee/organizations/components/OrganizationMemberAvatar.tsx create mode 100644 packages/lib/getAvatarUrl.ts diff --git a/apps/web/components/getting-started/steps-views/UserProfile.tsx b/apps/web/components/getting-started/steps-views/UserProfile.tsx index f197ff4461..79f4fd9076 100644 --- a/apps/web/components/getting-started/steps-views/UserProfile.tsx +++ b/apps/web/components/getting-started/steps-views/UserProfile.tsx @@ -3,12 +3,13 @@ import type { FormEvent } from "react"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; -import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; +import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import turndown from "@calcom/lib/turndownService"; import { trpc } from "@calcom/trpc/react"; +import type { Ensure } from "@calcom/types/utils"; import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui"; import { ArrowRight } from "@calcom/ui/components/icon"; @@ -96,16 +97,19 @@ const UserProfile = () => { }, ]; + const organization = + user.organization && user.organization.id + ? { + ...(user.organization as Ensure), + slug: user.organization.slug || null, + requestedSlug: user.organization.metadata?.requestedSlug || null, + } + : null; return (
{user && ( - + )} , "inviteToken">; type MembersType = TeamType["members"]; -type MemberType = Pick & { safeBio: string | null }; +type MemberType = Pick & { + safeBio: string | null; + orgOrigin: string; +}; const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => { const routerQuery = useRouterQuery(); @@ -20,9 +24,11 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery; return ( - +
- +

{member.name}

diff --git a/apps/web/components/ui/avatar/UserAvatar.tsx b/apps/web/components/ui/avatar/UserAvatar.tsx new file mode 100644 index 0000000000..63fa676676 --- /dev/null +++ b/apps/web/components/ui/avatar/UserAvatar.tsx @@ -0,0 +1,19 @@ +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import type { User } from "@calcom/prisma/client"; +import { Avatar } from "@calcom/ui"; + +type UserAvatarProps = Omit, "alt" | "imageSrc"> & { + user: Pick; + /** + * Useful when allowing the user to upload their own avatar and showing the avatar before it's uploaded + */ + previewSrc?: string | null; +}; + +/** + * It is aware of the user's organization to correctly show the avatar from the correct URL + */ +export function UserAvatar(props: UserAvatarProps) { + const { user, previewSrc, ...rest } = props; + return ; +} diff --git a/apps/web/components/ui/avatar/UserAvatarGroup.tsx b/apps/web/components/ui/avatar/UserAvatarGroup.tsx new file mode 100644 index 0000000000..ad3909641e --- /dev/null +++ b/apps/web/components/ui/avatar/UserAvatarGroup.tsx @@ -0,0 +1,20 @@ +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import type { User } from "@calcom/prisma/client"; +import { AvatarGroup } from "@calcom/ui"; + +type UserAvatarProps = Omit, "items"> & { + users: Pick[]; +}; +export function UserAvatarGroup(props: UserAvatarProps) { + const { users, ...rest } = props; + return ( + ({ + alt: user.name || "", + title: user.name || "", + image: getUserAvatarUrl(user), + }))} + /> + ); +} diff --git a/apps/web/components/ui/avatar/UserAvatarGroupWithOrg.tsx b/apps/web/components/ui/avatar/UserAvatarGroupWithOrg.tsx new file mode 100644 index 0000000000..9de57a0b57 --- /dev/null +++ b/apps/web/components/ui/avatar/UserAvatarGroupWithOrg.tsx @@ -0,0 +1,30 @@ +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import type { Team, User } from "@calcom/prisma/client"; +import { AvatarGroup } from "@calcom/ui"; + +type UserAvatarProps = Omit, "items"> & { + users: Pick[]; + organization: Pick; +}; + +export function UserAvatarGroupWithOrg(props: UserAvatarProps) { + const { users, organization, ...rest } = props; + const items = [ + { + image: `${WEBAPP_URL}/team/${organization.slug}/avatar.png`, + alt: organization.name || undefined, + title: organization.name, + }, + ].concat( + users.map((user) => { + return { + image: getUserAvatarUrl(user), + alt: user.name || undefined, + title: user.name || user.username || "", + }; + }) + ); + users.unshift(); + return ; +} diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 1392af4cfa..1e927a4539 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -11,7 +11,7 @@ import { useEmbedStyles, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; -import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; +import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; @@ -25,7 +25,7 @@ import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import prisma from "@calcom/prisma"; import { RedirectType, type EventType, type User } from "@calcom/prisma/client"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { HeadSeo, UnpublishedEntity } from "@calcom/ui"; import { Verified, ArrowRight } from "@calcom/ui/components/icon"; @@ -99,11 +99,22 @@ export function UserPage(props: InferGetServerSidePropsType
-

{profile.name} @@ -226,8 +237,13 @@ export type UserPageProps = { theme: string | null; brandColor: string; darkBrandColor: string; - organizationSlug: string | null; + organization: { + requestedSlug: string | null; + slug: string | null; + id: number | null; + }; allowSEOIndexing: boolean; + username: string | null; }; users: Pick[]; themeBasis: string | null; @@ -286,6 +302,7 @@ export const getServerSideProps: GetServerSideProps = async (cont select: { slug: true, name: true, + metadata: true, }, }, theme: true, @@ -313,6 +330,10 @@ export const getServerSideProps: GetServerSideProps = async (cont const users = usersWithoutAvatar.map((user) => ({ ...user, + organization: { + ...user.organization, + metadata: user.organization?.metadata ? teamMetadataSchema.parse(user.organization.metadata) : null, + }, avatar: `/${user.username}/avatar.png`, })); @@ -344,8 +365,13 @@ export const getServerSideProps: GetServerSideProps = async (cont theme: user.theme, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, - organizationSlug: user.organization?.slug ?? null, allowSEOIndexing: user.allowSEOIndexing ?? true, + username: user.username, + organization: { + id: user.organizationId, + slug: user.organization?.slug ?? null, + requestedSlug: user.organization?.metadata?.requestedSlug ?? null, + }, }; const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id); diff --git a/apps/web/pages/api/user/avatar.ts b/apps/web/pages/api/user/avatar.ts index fcf0ce7d09..6f6cabeaf7 100644 --- a/apps/web/pages/api/user/avatar.ts +++ b/apps/web/pages/api/user/avatar.ts @@ -1,15 +1,23 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; -import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { + orgDomainConfig, + whereClauseForOrgWithSlugOrRequestedSlug, +} from "@calcom/features/ee/organizations/lib/orgDomains"; import { AVATAR_FALLBACK } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; +const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); const querySchema = z .object({ username: z.string(), teamname: z.string(), + /** + * Passed when we want to fetch avatar of a particular organization + */ orgSlug: z.string(), /** * Allow fetching avatar of a particular organization @@ -30,11 +38,11 @@ async function getIdentityData(req: NextApiRequest) { id: orgId, } : org - ? getSlugOrRequestedSlug(org) + ? whereClauseForOrgWithSlugOrRequestedSlug(org) : null; if (username) { - let user = await prisma.user.findFirst({ + const user = await prisma.user.findFirst({ where: { username, organization: orgQuery, @@ -42,27 +50,6 @@ async function getIdentityData(req: NextApiRequest) { select: { avatar: true, email: true }, }); - /** - * TEMPORARY CODE STARTS - TO BE REMOVED after mono-user schema is implemented - * Try the non-org user temporarily to support users part of a team but not part of the organization - * This is needed because of a situation where we migrate a user and the team to ORG but not all the users in the team to the ORG. - * Eventually, all users will be migrated to the ORG but this is when user by user migration happens initially. - */ - // No user found in the org, try the non-org user that might be part of the team that's part of an org - if (!user && orgQuery) { - // The only side effect this code could have is that it could serve the avatar of a non-org member from the org domain but as long as the username isn't taken by an org member. - user = await prisma.user.findFirst({ - where: { - username, - organization: null, - }, - select: { avatar: true, email: true }, - }); - } - /** - * TEMPORARY CODE ENDS - */ - return { name: username, email: user?.email, @@ -79,6 +66,7 @@ async function getIdentityData(req: NextApiRequest) { }, select: { logo: true }, }); + return { org, name: teamname, @@ -86,15 +74,25 @@ async function getIdentityData(req: NextApiRequest) { avatar: getPlaceholderAvatar(team?.logo, teamname), }; } + if (orgSlug) { - const org = await prisma.team.findFirst({ - where: getSlugOrRequestedSlug(orgSlug), + const orgs = await prisma.team.findMany({ + where: { + ...whereClauseForOrgWithSlugOrRequestedSlug(orgSlug), + }, select: { slug: true, logo: true, name: true, }, }); + + if (orgs.length > 1) { + // This should never happen, but instead of throwing error, we are just logging to be able to observe when it happens. + log.error("More than one organization found for slug", orgSlug); + } + + const org = orgs[0]; return { org: org?.slug, name: org?.name, diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index 6b57135292..bfe82cdc27 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -6,7 +6,7 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; -import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar"; +import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; @@ -19,6 +19,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client"; import { trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react"; import type { AppRouter } from "@calcom/trpc/server/routers/_app"; +import type { Ensure } from "@calcom/types/utils"; import { Alert, Button, @@ -251,6 +252,7 @@ const ProfileView = () => { isLoading={updateProfileMutation.isLoading} isFallbackImg={checkIfItFallbackImage(fetchedImgSrc)} userAvatar={user.avatar} + user={user} userOrganization={user.organization} onSubmit={(values) => { if (values.email !== user.email && isCALIdentityProvider) { @@ -396,6 +398,7 @@ const ProfileForm = ({ isLoading = false, isFallbackImg, userAvatar, + user, userOrganization, }: { defaultValues: FormValues; @@ -404,6 +407,7 @@ const ProfileForm = ({ isLoading: boolean; isFallbackImg: boolean; userAvatar: string; + user: RouterOutputs["viewer"]["me"]; userOrganization: RouterOutputs["viewer"]["me"]["organization"]; }) => { const { t } = useLocale(); @@ -443,13 +447,21 @@ const ProfileForm = ({ name="avatar" render={({ field: { value } }) => { const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value); + const organization = + userOrganization && userOrganization.id + ? { + ...(userOrganization as Ensure), + slug: userOrganization.slug || null, + requestedSlug: userOrganization.metadata?.requestedSlug || null, + } + : null; return ( <> -

{t("profile_picture")}

diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 8aceea51ba..21f3459f1f 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -8,7 +8,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { z } from "zod"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; -import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; @@ -159,7 +159,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
- ({ - alt: user.name || "", - title: user.name || "", - image: `/${user.username}/avatar.png` || "", - }))} + users={type.users} />
@@ -149,17 +146,11 @@ function TeamPage({

- mem.subteams?.includes(ch.slug) && mem.accepted) - .map((member) => ({ - alt: member.name || "", - image: `/${member.username}/avatar.png`, - title: member.name || "", - }))} + users={team.members.filter((mem) => mem.subteams?.includes(ch.slug) && mem.accepted)} /> @@ -373,7 +364,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => subteams: member.subteams, username: member.username, accepted: member.accepted, + organizationId: member.organizationId, safeBio: markdownToSafeHTML(member.bio || ""), + orgOrigin: getOrgFullOrigin(member.organization?.slug || ""), }; }) : []; diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 6e21b15b96..97cc306065 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -8,7 +8,7 @@ import GoogleProvider from "next-auth/providers/google"; import checkLicense from "@calcom/features/ee/common/server/checkLicense"; import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; -import { getOrgFullDomain, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; @@ -471,7 +471,7 @@ export const AUTH_OPTIONS: AuthOptions = { id: organization.id, name: organization.name, slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "", - fullDomain: getOrgFullDomain(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""), + fullDomain: getOrgFullOrigin(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""), domainSuffix: subdomainSuffix(), } : undefined, diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx index 3101892eaa..29686e8f1f 100644 --- a/packages/features/bookings/components/event-meta/Members.tsx +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -1,9 +1,6 @@ -import { usePathname } from "next/navigation"; - -import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { SchedulingType } from "@calcom/prisma/enums"; -import { AvatarGroup } from "@calcom/ui"; +import { UserAvatarGroup } from "@calcom/web/components/ui/avatar/UserAvatarGroup"; +import { UserAvatarGroupWithOrg } from "@calcom/web/components/ui/avatar/UserAvatarGroupWithOrg"; import type { PublicEvent } from "../../types"; @@ -18,17 +15,7 @@ export interface EventMembersProps { entity: PublicEvent["entity"]; } -type Avatar = { - title: string; - image: string | undefined; - alt: string | undefined; - href: string | undefined; -}; - -type AvatarWithRequiredImage = Avatar & { image: string }; - export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => { - const pathname = usePathname(); const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN; const shownUsers = showMembers ? users : []; @@ -38,40 +25,22 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe !users.length || (profile.name !== users[0].name && schedulingType === SchedulingType.COLLECTIVE); - const avatars: Avatar[] = shownUsers.map((user) => ({ - title: `${user.name || user.username}`, - image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`, - alt: user.name || undefined, - href: `/${user.username}`, - })); - - // Add organization avatar - if (entity.orgSlug) { - avatars.unshift({ - title: `${entity.name}`, - image: `${WEBAPP_URL}/team/${entity.orgSlug}/avatar.png`, - alt: entity.name || undefined, - href: getOrgFullDomain(entity.orgSlug), - }); - } - - // Add profile later since we don't want to force creating an avatar for this if it doesn't exist. - avatars.unshift({ - title: `${profile.name || profile.username}`, - image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined, - alt: profile.name || undefined, - href: profile.username - ? `${CAL_URL}${pathname?.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}` - : undefined, - }); - - const uniqueAvatars = avatars - .filter((item): item is AvatarWithRequiredImage => !!item.image) - .filter((item, index, self) => self.findIndex((t) => t.image === item.image) === index); - return ( <> - + {entity.orgSlug ? ( + + ) : ( + + )} +

{showOnlyProfileName ? profile.name diff --git a/packages/features/ee/organizations/components/OrganizationAvatar.tsx b/packages/features/ee/organizations/components/OrganizationAvatar.tsx deleted file mode 100644 index 85a361a291..0000000000 --- a/packages/features/ee/organizations/components/OrganizationAvatar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import classNames from "@calcom/lib/classNames"; -import { Avatar } from "@calcom/ui"; -import type { AvatarProps } from "@calcom/ui"; - -type OrganizationAvatarProps = AvatarProps & { - organizationSlug: string | null | undefined; -}; - -const OrganizationAvatar = ({ size, imageSrc, alt, organizationSlug, ...rest }: OrganizationAvatarProps) => { - return ( - - {alt} -

- ) : null - } - /> - ); -}; - -export default OrganizationAvatar; diff --git a/packages/features/ee/organizations/components/OrganizationMemberAvatar.tsx b/packages/features/ee/organizations/components/OrganizationMemberAvatar.tsx new file mode 100644 index 0000000000..7c898776c7 --- /dev/null +++ b/packages/features/ee/organizations/components/OrganizationMemberAvatar.tsx @@ -0,0 +1,47 @@ +import classNames from "@calcom/lib/classNames"; +import { getOrgAvatarUrl } from "@calcom/lib/getAvatarUrl"; +// import { Avatar } from "@calcom/ui"; +import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar"; + +type OrganizationMemberAvatarProps = React.ComponentProps & { + organization: { + id: number; + slug: string | null; + requestedSlug: string | null; + } | null; +}; + +/** + * Shows the user's avatar along with a small organization's avatar + */ +const OrganizationMemberAvatar = ({ + size, + user, + organization, + previewSrc, + ...rest +}: OrganizationMemberAvatarProps) => { + return ( + + {user.username +
+ ) : null + } + {...rest} + /> + ); +}; + +export default OrganizationMemberAvatar; diff --git a/packages/features/ee/organizations/lib/orgDomains.ts b/packages/features/ee/organizations/lib/orgDomains.ts index 68f6425fad..8c55dd5929 100644 --- a/packages/features/ee/organizations/lib/orgDomains.ts +++ b/packages/features/ee/organizations/lib/orgDomains.ts @@ -1,6 +1,7 @@ import type { Prisma } from "@prisma/client"; import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; import slugify from "@calcom/lib/slugify"; /** @@ -18,6 +19,12 @@ export function getOrgSlug(hostname: string) { const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`; return testHostname.endsWith(`.${ahn}`); }); + logger.debug(`getOrgSlug: ${hostname} ${currentHostname}`, { + ALLOWED_HOSTNAMES, + WEBAPP_URL, + currentHostname, + hostname, + }); if (currentHostname) { // Define which is the current domain/subdomain const slug = hostname.replace(`.${currentHostname}` ?? "", ""); @@ -29,6 +36,7 @@ export function getOrgSlug(hostname: string) { export function orgDomainConfig(hostname: string, fallback?: string | string[]) { const currentOrgDomain = getOrgSlug(hostname); const isValidOrgDomain = currentOrgDomain !== null && !RESERVED_SUBDOMAINS.includes(currentOrgDomain); + logger.debug(`orgDomainConfig: ${hostname} ${currentOrgDomain} ${isValidOrgDomain}`); if (isValidOrgDomain || !fallback) { return { currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, @@ -48,10 +56,14 @@ export function subdomainSuffix() { return urlSplit.length === 3 ? urlSplit.slice(1).join(".") : urlSplit.join("."); } -export function getOrgFullDomain(slug: string, options: { protocol: boolean } = { protocol: true }) { +export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) { + if (!slug) return WEBAPP_URL; return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`; } +/** + * @deprecated You most probably intend to query for an organization only, use `whereClauseForOrgWithSlugOrRequestedSlug` instead which will only return the organization and not a team accidentally. + */ export function getSlugOrRequestedSlug(slug: string) { const slugifiedValue = slugify(slug); return { @@ -67,6 +79,26 @@ export function getSlugOrRequestedSlug(slug: string) { } satisfies Prisma.TeamWhereInput; } +export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) { + const slugifiedValue = slugify(slug); + + return { + OR: [ + { slug: slugifiedValue }, + { + metadata: { + path: ["requestedSlug"], + equals: slug, + }, + }, + ], + metadata: { + path: ["isOrganization"], + equals: true, + }, + } satisfies Prisma.TeamWhereInput; +} + export function userOrgQuery(hostname: string, fallback?: string | string[]) { const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(hostname, fallback); return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null; diff --git a/packages/features/ee/teams/components/MemberListItem.tsx b/packages/features/ee/teams/components/MemberListItem.tsx index e460d52e46..1bfaa68b60 100644 --- a/packages/features/ee/teams/components/MemberListItem.tsx +++ b/packages/features/ee/teams/components/MemberListItem.tsx @@ -11,7 +11,6 @@ import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { - Avatar, Button, ButtonGroup, ConfirmationDialogContent, @@ -29,6 +28,7 @@ import { Tooltip, } from "@calcom/ui"; import { ExternalLink, MoreHorizontal, Edit2, Lock, UserX } from "@calcom/ui/components/icon"; +import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar"; import MemberChangeRoleModal from "./MemberChangeRoleModal"; import TeamAvailabilityModal from "./TeamAvailabilityModal"; @@ -141,13 +141,7 @@ export default function MemberListItem(props: Props) {
- - +
{name} diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index d9cd6ab6e2..69179974d0 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -8,7 +8,7 @@ import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -235,7 +235,7 @@ const ProfileView = () => { value={value} addOnLeading={ team.parent && orgBranding - ? `${getOrgFullDomain(orgBranding?.slug, { protocol: false })}/` + ? `${getOrgFullOrigin(orgBranding?.slug, { protocol: false })}/` : `${WEBAPP_URL}/team/` } onChange={(e) => { diff --git a/packages/features/ee/users/server/trpc-router.ts b/packages/features/ee/users/server/trpc-router.ts index fd4fec9115..d0789d81da 100644 --- a/packages/features/ee/users/server/trpc-router.ts +++ b/packages/features/ee/users/server/trpc-router.ts @@ -33,6 +33,9 @@ const userBodySchema = User.pick({ avatar: true, }); +/** + * @deprecated in favour of @calcom/lib/getAvatarUrl + */ /** This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url */ export function getAvatarUrlFromUser(user: { avatar: string | null; diff --git a/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx b/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx index d6cc851900..328644da30 100644 --- a/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx +++ b/packages/features/eventtypes/components/ChildrenEventTypeSelect.tsx @@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { Props } from "react-select"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { classNames } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -63,7 +63,7 @@ export const ChildrenEventTypeSelect = ({ ()({ brandColor: true, darkBrandColor: true, theme: true, + organizationId: true, metadata: true, }, }, @@ -93,6 +94,7 @@ const publicEventSelect = Prisma.validator()({ metadata: true, brandColor: true, darkBrandColor: true, + organizationId: true, organization: { select: { name: true, @@ -130,6 +132,7 @@ export const getPublicEvent = async ( brandColor: true, darkBrandColor: true, theme: true, + organizationId: true, organization: { select: { slug: true, @@ -291,23 +294,24 @@ function getUsersFromEvent(event: Event) { if (!owner) { return null; } - const { username, name, weekStart } = owner; - return [{ username, name, weekStart }]; + const { username, name, weekStart, organizationId } = owner; + return [{ username, name, weekStart, organizationId }]; } async function getOwnerFromUsersArray(prisma: PrismaClient, eventTypeId: number) { const { users } = await prisma.eventType.findUniqueOrThrow({ where: { id: eventTypeId }, - select: { users: { select: { username: true, name: true, weekStart: true } } }, + select: { users: { select: { username: true, name: true, weekStart: true, organizationId: true } } }, }); if (!users.length) return null; return [users[0]]; } -function mapHostsToUsers(host: { user: Pick }) { +function mapHostsToUsers(host: { user: Pick }) { return { username: host.user.username, name: host.user.name, weekStart: host.user.weekStart, + organizationId: host.user.organizationId, }; } diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index af669bd647..0975f4d055 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -12,7 +12,7 @@ import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge"; import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner"; import { OrgUpgradeBanner } from "@calcom/features/ee/organizations/components/OrgUpgradeBanner"; -import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem"; import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components"; import { useFlagMap } from "@calcom/features/flags/context/provider"; @@ -797,7 +797,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) { const publicPageUrl = useMemo(() => { if (!user?.org?.id) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`; - const publicPageUrl = orgBranding?.slug ? getOrgFullDomain(orgBranding.slug) : ""; + const publicPageUrl = orgBranding?.slug ? getOrgFullOrigin(orgBranding.slug) : ""; return publicPageUrl; }, [orgBranding?.slug, user?.username, user?.org?.id]); diff --git a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx index bdedad5d1c..d1e5b103b9 100644 --- a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx +++ b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx @@ -8,7 +8,8 @@ import type { DateRange } from "@calcom/lib/date-ranges"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { MembershipRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc"; -import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui"; +import { Button, ButtonGroup, DataTable } from "@calcom/ui"; +import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar"; import { UpgradeTip } from "../../tips/UpgradeTip"; import { TBContext, createTimezoneBuddyStore } from "../store"; @@ -18,6 +19,8 @@ import { TimeDial } from "./TimeDial"; export interface SliderUser { id: number; username: string | null; + name: string | null; + organizationId: number; email: string; timeZone: string; role: MembershipRole; @@ -78,10 +81,17 @@ export function AvailabilitySliderTable() { accessorFn: (data) => data.email, header: "Member", cell: ({ row }) => { - const { username, email, timeZone } = row.original; + const { username, email, timeZone, name, organizationId } = row.original; return (
- +
{username || "No username"} diff --git a/packages/lib/getAvatarUrl.ts b/packages/lib/getAvatarUrl.ts new file mode 100644 index 0000000000..2c971be827 --- /dev/null +++ b/packages/lib/getAvatarUrl.ts @@ -0,0 +1,24 @@ +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { AVATAR_FALLBACK } from "@calcom/lib/constants"; +import type { User, Team } from "@calcom/prisma/client"; + +/** + * Gives an organization aware avatar url for a user + * It ensures that the wrong avatar isn't fetched by ensuring that organizationId is always passed + */ +export const getUserAvatarUrl = (user: Pick) => { + if (!user.username) return AVATAR_FALLBACK; + // avatar.png automatically redirects to fallback avatar if user doesn't have one + return `${WEBAPP_URL}/${user.username}/avatar.png${ + user.organizationId ? `?orgId=${user.organizationId}` : "" + }`; +}; + +export const getOrgAvatarUrl = (org: { + id: Team["id"]; + slug: Team["slug"]; + requestedSlug: string | null; +}) => { + const slug = org.slug ?? org.requestedSlug; + return `${WEBAPP_URL}/org/${slug}/avatar.png`; +}; diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 3f96aa9e36..7637446d80 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -4,7 +4,7 @@ import { getLocationGroupedOptions } from "@calcom/app-store/server"; import type { StripeData } from "@calcom/app-store/stripepayment/lib/server"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import type { LocationObject } from "@calcom/core/location"; -import { getOrgFullDomain } from "@calcom/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; @@ -298,7 +298,7 @@ export default async function getEventTypeById({ const eventTypeUsers: ((typeof eventType.users)[number] & { avatar: string })[] = eventType.users.map( (user) => ({ ...user, - avatar: `${eventType.team?.parent?.slug ? getOrgFullDomain(eventType.team?.parent?.slug) : CAL_URL}/${ + avatar: `${eventType.team?.parent?.slug ? getOrgFullOrigin(eventType.team?.parent?.slug) : CAL_URL}/${ user.username }/avatar.png`, }) @@ -348,7 +348,7 @@ export default async function getEventTypeById({ ...member.user, avatar: `${ eventTypeObject.team?.parent?.slug - ? getOrgFullDomain(eventTypeObject.team?.parent?.slug) + ? getOrgFullOrigin(eventTypeObject.team?.parent?.slug) : CAL_URL }/${member.user.username}/avatar.png`, }; diff --git a/packages/lib/server/getBrand.ts b/packages/lib/server/getBrand.ts index 98ac5d4b3e..ad4066e9f1 100644 --- a/packages/lib/server/getBrand.ts +++ b/packages/lib/server/getBrand.ts @@ -1,4 +1,4 @@ -import { subdomainSuffix, getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { subdomainSuffix, getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import { prisma } from "@calcom/prisma"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -19,7 +19,7 @@ export const getBrand = async (orgId: number | null) => { }); const metadata = teamMetadataSchema.parse(org?.metadata); const slug = (org?.slug || metadata?.requestedSlug) as string; - const fullDomain = getOrgFullDomain(slug); + const fullDomain = getOrgFullOrigin(slug); const domainSuffix = subdomainSuffix(); return { diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts index 2d1fe4189b..62e2411618 100644 --- a/packages/lib/server/queries/teams/index.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; import { getAppFromSlug } from "@calcom/app-store/utils"; -import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains"; +import { getOrgFullOrigin, getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains"; import prisma, { baseEventTypeSelect } from "@calcom/prisma"; import { SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -30,6 +30,12 @@ export async function getTeamWithMembers(args: { name: true, id: true, bio: true, + organizationId: true, + organization: { + select: { + slug: true, + }, + }, teams: { select: { team: { @@ -163,6 +169,7 @@ export async function getTeamWithMembers(args: { ? obj.user.teams.filter((obj) => obj.team.slug !== orgSlug).map((obj) => obj.team.slug) : null, avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`, + orgOrigin: getOrgFullOrigin(obj.user.organization?.slug || ""), connectedApps: !isTeamView ? credentials?.map((cred) => { const appSlug = cred.app?.slug; diff --git a/packages/trpc/server/routers/loggedInViewer/me.handler.ts b/packages/trpc/server/routers/loggedInViewer/me.handler.ts index 8463fc4058..3b53cfa0c6 100644 --- a/packages/trpc/server/routers/loggedInViewer/me.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/me.handler.ts @@ -1,5 +1,4 @@ -import { getOrgFullDomain } from "@calcom/ee/organizations/lib/orgDomains"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; type MeOptions = { @@ -25,9 +24,7 @@ export const meHandler = async ({ ctx }: MeOptions) => { locale: user.locale, timeFormat: user.timeFormat, timeZone: user.timeZone, - avatar: `${user.organization?.slug ? getOrgFullDomain(user.organization.slug) : WEBAPP_URL}/${ - user.username - }/avatar.png`, + avatar: getUserAvatarUrl(user), createdDate: user.createdDate, trialEndsAt: user.trialEndsAt, defaultScheduleId: user.defaultScheduleId, diff --git a/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts b/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts index 3213573854..96f20707a1 100644 --- a/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/team/listTeamAvailability.handler.ts @@ -41,7 +41,9 @@ async function getTeamMembers({ user: { select: { id: true, + organizationId: true, username: true, + name: true, email: true, timeZone: true, defaultScheduleId: true, @@ -63,6 +65,8 @@ async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) { if (!member.user.defaultScheduleId) { return { id: member.user.id, + organizationId: member.user.organizationId, + name: member.user.name, username: member.user.username, email: member.user.email, timeZone: member.user.timeZone, @@ -89,6 +93,8 @@ async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) { id: member.user.id, username: member.user.username, email: member.user.email, + organizationId: member.user.organizationId, + name: member.user.name, timeZone, role: member.role, defaultScheduleId: member.user.defaultScheduleId ?? -1, From 9250b91bb0d5a66ccf2cf42311ac9999c79f6a84 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:59:54 +0530 Subject: [PATCH 044/118] feat: remove location modal in event setup (#11796) Co-authored-by: Peer Richelsen --- .../components/eventtype/EventSetupTab.tsx | 503 ++++++++++-------- apps/web/components/ui/form/CheckboxField.tsx | 2 +- apps/web/pages/event-types/[type]/index.tsx | 23 + apps/web/playwright/event-types.e2e.ts | 126 ++++- apps/web/public/static/locales/en/common.json | 1 + 5 files changed, 408 insertions(+), 247 deletions(-) diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index a5a386a2d3..754f060868 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -1,27 +1,22 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { isValidPhoneNumber } from "libphonenumber-js"; +import { ErrorMessage } from "@hookform/error-message"; import { Trans } from "next-i18next"; import Link from "next/link"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useEffect, useState } from "react"; -import { Controller, useForm, useFormContext } from "react-hook-form"; +import { Controller, useFormContext, useFieldArray } from "react-hook-form"; import type { MultiValue } from "react-select"; -import { z } from "zod"; import type { EventLocationType } from "@calcom/app-store/locations"; -import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations"; +import { getEventLocationType, LocationType, MeetLocationType } 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 { classNames } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import invertLogoOnDark from "@calcom/lib/invertLogoOnDark"; import { md } from "@calcom/lib/markdownIt"; import { slugify } from "@calcom/lib/slugify"; import turndown from "@calcom/lib/turndownService"; import { - Button, Label, Select, SettingsToggle, @@ -30,11 +25,16 @@ import { Editor, SkeletonContainer, SkeletonText, + Input, + PhoneInput, + Button, + showToast, } from "@calcom/ui"; -import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon"; +import { Plus, X, Check } from "@calcom/ui/components/icon"; +import { CornerDownRight } from "@calcom/ui/components/icon"; -import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; -import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect"; +import CheckboxField from "@components/ui/form/CheckboxField"; +import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect"; import LocationSelect from "@components/ui/form/LocationSelect"; const getLocationFromType = ( @@ -114,9 +114,6 @@ export const EventSetupTab = ( const { t } = useLocale(); const formMethods = useFormContext(); const { eventType, team, destinationCalendar } = props; - const [showLocationModal, setShowLocationModal] = useState(false); - const [editingLocationType, setEditingLocationType] = useState(""); - const [selectedLocation, setSelectedLocation] = useState(undefined); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration); const orgBranding = useOrgBranding(); const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled"); @@ -150,83 +147,6 @@ export const EventSetupTab = ( selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null ); - const openLocationModal = (type: EventLocationType["type"], address = "") => { - const option = getLocationFromType(type, locationOptions); - if (option && option.value === LocationType.InPerson) { - const inPersonOption = { - ...option, - address, - }; - setSelectedLocation(inPersonOption); - } else { - setSelectedLocation(option); - } - setShowLocationModal(true); - }; - - const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => { - formMethods.setValue( - "locations", - formMethods.getValues("locations").filter((location) => { - if (location.type === LocationType.InPerson) { - return location.address !== selectedLocation.address; - } - return location.type !== selectedLocation.type; - }), - { shouldValidate: true } - ); - }; - - const saveLocation = (newLocationType: EventLocationType["type"], details = {}) => { - const locationType = editingLocationType !== "" ? editingLocationType : newLocationType; - const existingIdx = formMethods.getValues("locations").findIndex((loc) => locationType === loc.type); - if (existingIdx !== -1) { - const copy = formMethods.getValues("locations"); - if (editingLocationType !== "") { - copy[existingIdx] = { - ...details, - type: newLocationType, - }; - } - - formMethods.setValue("locations", [ - ...copy, - ...(newLocationType === LocationType.InPerson && editingLocationType === "" - ? [{ ...details, type: newLocationType }] - : []), - ]); - } else { - formMethods.setValue( - "locations", - formMethods.getValues("locations").concat({ type: newLocationType, ...details }) - ); - } - - setEditingLocationType(""); - setShowLocationModal(false); - }; - - const locationFormSchema = z.object({ - locationType: z.string(), - locationAddress: z.string().optional(), - displayLocationPublicly: z.boolean().optional(), - locationPhoneNumber: z - .string() - .refine((val) => isValidPhoneNumber(val)) - .optional(), - locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field - }); - - const locationFormMethods = useForm<{ - locationType: EventLocationType["type"]; - locationPhoneNumber?: string; - locationAddress?: string; // TODO: We should validate address or fetch the address from googles api to see if its valid? - locationLink?: string; // Currently this only accepts links that are HTTPS:// - displayLocationPublicly?: boolean; - }>({ - resolver: zodResolver(locationFormSchema), - }); - const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager( eventType, @@ -236,6 +156,15 @@ export const EventSetupTab = ( const Locations = () => { const { t } = useLocale(); + const { + fields: locationFields, + append, + remove, + update: updateLocationField, + } = useFieldArray({ + control: formMethods.control, + name: "locations", + }); const [animationRef] = useAutoAnimate(); @@ -254,131 +183,266 @@ export const EventSetupTab = ( const { locationDetails, locationAvailable } = getLocationInfo(props); + const LocationInput = (props: { + eventLocationType: EventLocationType; + defaultValue?: string; + index: number; + }) => { + const { eventLocationType, index, ...remainingProps } = props; + + if (eventLocationType?.organizerInputType === "text") { + const { defaultValue, ...rest } = remainingProps; + + return ( + { + return ( + <> + + + + ); + }} + /> + ); + } else if (eventLocationType?.organizerInputType === "phone") { + const { defaultValue, ...rest } = remainingProps; + + return ( + { + return ( + <> + + + + ); + }} + /> + ); + } + return null; + }; + + const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false); + const [selectedNewOption, setSelectedNewOption] = useState(null); + return (
- {validLocations.length === 0 && ( -
- { - if (e?.value) { - const newLocationType = e.value; - const eventLocationType = getEventLocationType(newLocationType); - if (!eventLocationType) { - return; - } - locationFormMethods.setValue("locationType", newLocationType); - if (eventLocationType.organizerInputType) { - openLocationModal(newLocationType); - } else { - saveLocation(newLocationType); - } +
    + {locationFields.map((field, index) => { + const eventLocationType = getEventLocationType(field.type); + const defaultLocation = formMethods + .getValues("locations") + ?.find((location: { type: EventLocationType["type"]; address?: string }) => { + if (location.type === LocationType.InPerson) { + return location.type === eventLocationType?.type && location.address === field?.address; + } else { + return location.type === eventLocationType?.type; } - }} - /> -
- )} - {validLocations.length > 0 && ( -
    - {validLocations.map((location, index) => { - const eventLocationType = getEventLocationType(location.type); - if (!eventLocationType) { - return null; - } + }); - const eventLabel = - location[eventLocationType.defaultValueVariable] || t(eventLocationType.label); - return ( -
  • -
    -
    - {`${eventLocationType.label} - {`${eventLabel} ${ - location.teamName ? `(${location.teamName})` : "" - }`} + const option = getLocationFromType(field.type, locationOptions); + + return ( +
  • +
    + { + if (e?.value) { + const newLocationType = e.value; + const eventLocationType = getEventLocationType(newLocationType); + if (!eventLocationType) { + return; + } + const canAddLocation = + eventLocationType.organizerInputType || + !validLocations.find((location) => location.type === newLocationType); + + if (canAddLocation) { + updateLocationField(index, { type: newLocationType }); + } else { + updateLocationField(index, { type: field.type }); + showToast(t("location_already_exists"), "warning"); + } + } + }} + /> + +
    + + {eventLocationType?.organizerInputType && ( +
    +
    +
    + +
    +
    + +
    +
    +
    + { + const fieldValues = formMethods.getValues().locations[index]; + updateLocationField(index, { + ...fieldValues, + displayLocationPublicly: e.target.checked, + }); }} - aria-label={t("edit")} - className="hover:text-emphasis text-subtle mr-1 p-1"> - - - + informationIconText={t("display_location_info_badge")} + />
    -
  • - ); - })} - {validLocations.some( - (location) => - location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar" - ) && ( -
    - - -

    - The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work. - Change it{" "} - - here. - {" "} -

    -
    -
    - )} - {isChildrenManagedEventType && !locationAvailable && locationDetails && ( -

    - {t("app_not_connected", { appName: locationDetails.name })}{" "} - - {t("connect_now")} - -

    - )} - {validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && ( -
  • - + )}
  • - )} -
- )} + ); + })} + {(validLocations.length === 0 || showEmptyLocationSelect) && ( +
+ { + if (e?.value) { + const newLocationType = e.value; + const eventLocationType = getEventLocationType(newLocationType); + if (!eventLocationType) { + return; + } + + const canAppendLocation = + eventLocationType.organizerInputType || + !validLocations.find((location) => location.type === newLocationType); + + if (canAppendLocation) { + append({ type: newLocationType }); + setSelectedNewOption(e); + } else { + showToast(t("location_already_exists"), "warning"); + setSelectedNewOption(null); + } + } + }} + /> +
+ )} + {validLocations.some( + (location) => + location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar" + ) && ( +
+
+ +
+ +

+ The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work. + Change it{" "} + + here. + {" "} +

+
+
+ )} + {isChildrenManagedEventType && !locationAvailable && locationDetails && ( +

+ {t("app_not_connected", { appName: locationDetails.name })}{" "} + + {t("connect_now")} + +

+ )} + {validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && ( +
  • + +
  • + )} + +

    + + Can't find the right video app? Visit our + + App Store + + . + +

    ); }; @@ -542,33 +606,6 @@ export const EventSetupTab = ( />
    - - {/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */} -
    ); diff --git a/apps/web/components/ui/form/CheckboxField.tsx b/apps/web/components/ui/form/CheckboxField.tsx index 222cbd7731..8298fbb5b5 100644 --- a/apps/web/components/ui/form/CheckboxField.tsx +++ b/apps/web/components/ui/form/CheckboxField.tsx @@ -52,7 +52,7 @@ const CheckboxField = forwardRef( className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded" />
    - {description} + {description} )} {informationIconText && } diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 1c0df98e0a..3c944f399e 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; +import { isValidPhoneNumber } from "libphonenumber-js"; import type { GetServerSidePropsContext } from "next"; import dynamic from "next/dynamic"; import { useEffect, useMemo, useState } from "react"; @@ -299,6 +300,28 @@ const EventTypePage = (props: EventTypeSetupProps) => { length: z.union([z.string().transform((val) => +val), z.number()]).optional(), offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(), bookingFields: eventTypeBookingFields, + locations: z + .array( + z + .object({ + type: z.string(), + address: z.string().optional(), + link: z.string().url().optional(), + phone: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + hostPhoneNumber: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + displayLocationPublicly: z.boolean().optional(), + credentialId: z.number().optional(), + teamName: z.string().optional(), + }) + .passthrough() + ) + .optional(), }) // TODO: Add schema for other fields later. .passthrough() diff --git a/apps/web/playwright/event-types.e2e.ts b/apps/web/playwright/event-types.e2e.ts index a5af946dda..70e7e18b88 100644 --- a/apps/web/playwright/event-types.e2e.ts +++ b/apps/web/playwright/event-types.e2e.ts @@ -115,23 +115,13 @@ test.describe("Event Types tests", () => { const locationData = ["location 1", "location 2", "location 3"]; - const fillLocation = async (inputText: string) => { - await page.locator("#location-select").click(); - await page.locator("text=In Person (Organizer Address)").click(); - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(1000); - await page.locator('input[name="locationAddress"]').fill(inputText); - await page.locator("[data-testid=display-location]").check(); - await page.locator("[data-testid=update-location]").click(); - }; - - await fillLocation(locationData[0]); + await fillLocation(page, locationData[0], 0); await page.locator("[data-testid=add-location]").click(); - await fillLocation(locationData[1]); + await fillLocation(page, locationData[1], 1); await page.locator("[data-testid=add-location]").click(); - await fillLocation(locationData[2]); + await fillLocation(page, locationData[2], 2); await page.locator("[data-testid=update-eventtype]").click(); @@ -177,6 +167,93 @@ test.describe("Event Types tests", () => { await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("text=+19199999999")).toBeVisible(); }); + + test("Can add Organzer Phone Number location and book with it", async ({ page }) => { + await gotoFirstEventType(page); + + await page.locator("#location-select").click(); + await page.locator(`text="Organizer Phone Number"`).click(); + const locationInputName = "locations[0].hostPhoneNumber"; + await page.locator(`input[name="${locationInputName}"]`).waitFor(); + await page.locator(`input[name="${locationInputName}"]`).fill("9199999999"); + + await saveEventType(page); + await gotoBookingPage(page); + await selectFirstAvailableTimeSlotNextMonth(page); + + await bookTimeSlot(page); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + await expect(page.locator("text=+19199999999")).toBeVisible(); + }); + + test("Can add Cal video location and book with it", async ({ page }) => { + await gotoFirstEventType(page); + + await page.locator("#location-select").click(); + await page.locator(`text="Cal Video (Global)"`).click(); + + await saveEventType(page); + await page.getByTestId("toast-success").waitFor(); + await gotoBookingPage(page); + await selectFirstAvailableTimeSlotNextMonth(page); + + await bookTimeSlot(page); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + await expect(page.locator("[data-testid=where] ")).toContainText("Cal Video"); + }); + + test("Can add Link Meeting as location and book with it", async ({ page }) => { + await gotoFirstEventType(page); + + await page.locator("#location-select").click(); + await page.locator(`text="Link meeting"`).click(); + + const locationInputName = `locations[0].link`; + + const testUrl = "https://cal.ai/"; + await page.locator(`input[name="${locationInputName}"]`).fill(testUrl); + + await saveEventType(page); + await page.getByTestId("toast-success").waitFor(); + await gotoBookingPage(page); + await selectFirstAvailableTimeSlotNextMonth(page); + + await bookTimeSlot(page); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + const linkElement = await page.locator("[data-testid=where] > a"); + expect(await linkElement.getAttribute("href")).toBe(testUrl); + }); + + test("Can remove location from multiple locations that are saved", async ({ page }) => { + await gotoFirstEventType(page); + + // Add Attendee Phone Number location + await selectAttendeePhoneNumber(page); + + // Add Cal Video location + await addAnotherLocation(page, "Cal Video (Global)"); + + await saveEventType(page); + await page.waitForLoadState("networkidle"); + + // Remove Attendee Phone Number Location + const removeButtomId = "delete-locations.0.type"; + await page.getByTestId(removeButtomId).click(); + + await saveEventType(page); + await page.waitForLoadState("networkidle"); + + await gotoBookingPage(page); + await selectFirstAvailableTimeSlotNextMonth(page); + + await bookTimeSlot(page); + + await expect(page.locator("[data-testid=success-page]")).toBeVisible(); + await expect(page.locator("[data-testid=where]")).toHaveText(/Cal Video/); + }); }); }); }); @@ -205,3 +282,26 @@ async function gotoBookingPage(page: Page) { await page.goto(previewLink ?? ""); } + +/** + * Adds n+1 location to the event type + */ +async function addAnotherLocation(page: Page, locationOptionText: string) { + await page.locator("[data-testid=add-location]").click(); + // When adding another location, the dropdown opens automatically. So, we don't need to open it here. + // + await page.locator(`text="${locationOptionText}"`).click(); +} + +const fillLocation = async (page: Page, inputText: string, index: number) => { + // Except the first location, dropdown automatically opens when adding another location + if (index == 0) { + await page.locator("#location-select").last().click(); + } + await page.locator("text=In Person (Organizer Address)").last().click(); + + const locationInputName = `locations[${index}].address`; + await page.locator(`input[name="${locationInputName}"]`).waitFor(); + await page.locator(`input[name="locations[${index}].address"]`).fill(inputText); + await page.locator("[data-testid=display-location]").last().check(); +}; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 7a1b81c44c..7a78643903 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1605,6 +1605,7 @@ "options": "Options", "enter_option": "Enter Option {{index}}", "add_an_option": "Add an option", + "location_already_exists": "This Location already exists. Please select a new location", "radio": "Radio", "google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar", "individual": "Individual", From bf6dd665f0df561ec63c64337d8ea765d6e77427 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:27:30 -0400 Subject: [PATCH 045/118] fix: response size scheduleEmailReminder (#12057) Co-authored-by: CarinaWolli --- .../ee/workflows/api/scheduleEmailReminders.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 3ce801fb0f..5755d7149e 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -14,6 +14,7 @@ import { parseRecurringEvent } from "@calcom/lib"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; +import type { User } from "@calcom/prisma/client"; import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -29,14 +30,14 @@ sgMail.setApiKey(sendgridAPIKey); type Booking = Prisma.BookingGetPayload<{ include: { eventType: true; - user: true; attendees: true; }; }>; function getiCalEventAsString( - booking: Pick & { + booking: Pick & { eventType: { recurringEvent?: Prisma.JsonValue; title?: string } | null; + user: Partial | null; } ) { let recurrenceRule: string | undefined = undefined; @@ -234,7 +235,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { endTime: true, location: true, description: true, - user: true, + user: { + select: { + email: true, + name: true, + timeZone: true, + locale: true, + username: true, + timeFormat: true, + hideBranding: true, + }, + }, metadata: true, uid: true, customInputs: true, From 687669ce179fbc44d4749fe8a9b85752fd846b3e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 24 Oct 2023 15:31:04 +0000 Subject: [PATCH 046/118] New Crowdin translations by Github Action --- apps/web/public/static/locales/fr/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 9d7cb5be31..6aa814498a 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -1600,6 +1600,7 @@ "options": "Options", "enter_option": "Entrer l'option {{index}}", "add_an_option": "Ajouter une option", + "location_already_exists": "Ce lieu existe déjà. Veuillez en sélectionner un nouveau.", "radio": "Radio", "google_meet_warning": "Pour utiliser Google Meet, vous devez définir votre calendrier de destination sur un calendrier Google", "individual": "Particulier", From a8c03262c2db4d4e35d198a765b69191ab0f5d0c Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Tue, 24 Oct 2023 20:15:17 +0100 Subject: [PATCH 047/118] fix: re-render on booker (#12058) --- .../bookings/Booker/components/EventMeta.tsx | 8 -------- .../bookings/components/AvailableTimes.tsx | 14 ++++++++------ .../bookings/lib/useCheckOverlapWithOverlay.tsx | 10 +++++++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 6a6fb5ee78..83bcfc6312 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -1,6 +1,5 @@ import { m } from "framer-motion"; import dynamic from "next/dynamic"; -import { useEffect } from "react"; import { shallow } from "zustand/shallow"; import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; @@ -38,13 +37,6 @@ export const EventMeta = () => { const isEmbed = useIsEmbed(); const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false; - useEffect(() => { - if (!selectedDuration && event?.length) { - setSelectedDuration(event.length); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [event?.length, selectedDuration]); - if (hideEventTypeDetails) { return null; } diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 509056d5a3..1daa55e284 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -11,6 +11,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button, SkeletonText } from "@calcom/ui"; import { useBookerStore } from "../Booker/store"; +import { useEvent } from "../Booker/utils/event"; import { getQueryParam } from "../Booker/utils/query-param"; import { useTimePreferences } from "../lib"; import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay"; @@ -51,9 +52,9 @@ const SlotItem = ({ const overlayCalendarToggled = getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault"); const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); - const selectedDuration = useBookerStore((state) => state.selectedDuration); const bookingData = useBookerStore((state) => state.bookingData); const layout = useBookerStore((state) => state.layout); + const { data: event } = useEvent(); const hasTimeSlots = !!seatsPerTimeSlot; const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone); @@ -67,11 +68,12 @@ const SlotItem = ({ const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60; - const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay( - computedDateWithUsersTimezone, - selectedDuration, - offset - ); + const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({ + start: computedDateWithUsersTimezone, + selectedDuration: event?.length ?? 0, + offset, + }); + const [overlapConfirm, setOverlapConfirm] = useState(false); const onButtonClick = useCallback(() => { diff --git a/packages/features/bookings/lib/useCheckOverlapWithOverlay.tsx b/packages/features/bookings/lib/useCheckOverlapWithOverlay.tsx index a1a3020da8..ba994ee7f7 100644 --- a/packages/features/bookings/lib/useCheckOverlapWithOverlay.tsx +++ b/packages/features/bookings/lib/useCheckOverlapWithOverlay.tsx @@ -9,7 +9,15 @@ function getCurrentTime(date: Date) { return `${hours}:${minutes}`; } -export function useCheckOverlapWithOverlay(start: Dayjs, selectedDuration: number | null, offset: number) { +export function useCheckOverlapWithOverlay({ + start, + selectedDuration, + offset, +}: { + start: Dayjs; + selectedDuration: number | null; + offset: number; +}) { const overlayBusyDates = useOverlayCalendarStore((state) => state.overlayBusyDates); let overlappingTimeStart: string | null = null; From 0ae6506bc13bd720ea3583504f9cfb89237aa8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Tue, 24 Oct 2023 12:59:15 -0700 Subject: [PATCH 048/118] fix: prevents prisma idle connections (#12068) --- packages/prisma/index.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/prisma/index.ts b/packages/prisma/index.ts index 071f6fdbbe..b045ff8fb6 100644 --- a/packages/prisma/index.ts +++ b/packages/prisma/index.ts @@ -6,9 +6,16 @@ import { bookingReferenceMiddleware } from "./middleware"; const prismaOptions: Prisma.PrismaClientOptions = {}; +const globalForPrisma = global as unknown as { + prismaWithoutClientExtensions: PrismaClientWithoutExtension; + prismaWithClientExtensions: PrismaClientWithExtensions; +}; + if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"]; -const prismaWithoutClientExtensions = new PrismaClientWithoutExtension(prismaOptions); +// Prevents flooding with idle connections +const prismaWithoutClientExtensions = + globalForPrisma.prismaWithoutClientExtensions || new PrismaClientWithoutExtension(prismaOptions); export const customPrisma = (options?: Prisma.PrismaClientOptions) => new PrismaClientWithoutExtension({ ...prismaOptions, ...options }).$extends(withAccelerate()); @@ -50,16 +57,15 @@ const prismaWithClientExtensions = prismaWithoutClientExtensions // }, // }) -// const prismaWithClientExtensions = prismaWithoutClientExtensions; - -export const prisma = - ((globalThis as any).prisma as typeof prismaWithClientExtensions) || prismaWithClientExtensions; +export const prisma = globalForPrisma.prismaWithClientExtensions || prismaWithClientExtensions; if (process.env.NODE_ENV !== "production") { - (globalThis as any).prisma = prisma; + globalForPrisma.prismaWithoutClientExtensions = prismaWithoutClientExtensions; + globalForPrisma.prismaWithClientExtensions = prisma; } -export type PrismaClient = typeof prismaWithClientExtensions; +type PrismaClientWithExtensions = typeof prismaWithClientExtensions; +export type PrismaClient = PrismaClientWithExtensions; export default prisma; export * from "./selects"; From a9535d3fd4590959599bfc7565092ebec0cf5879 Mon Sep 17 00:00:00 2001 From: Greg Pabian <35925521+grzpab@users.noreply.github.com> Date: Tue, 24 Oct 2023 22:52:59 +0200 Subject: [PATCH 049/118] chore: [app dir bootstrapping 4.1] check nullability of navigation hook return values part 2 (#12065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Omar López --- .../OverlayCalendar/OverlayCalendarSettingsModal.tsx | 2 +- packages/features/ee/payments/components/Payment.tsx | 2 +- packages/features/embed/Embed.tsx | 6 +++--- packages/lib/payment/handlePayment.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx index 118265be69..f5731fd17d 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSettingsModal.tsx @@ -47,7 +47,7 @@ export function OverlayCalendarSettingsModal(props: IOverlayCalendarContinueModa const searchParams = useSearchParams(); const setOverlayBusyDates = useOverlayCalendarStore((state) => state.setOverlayBusyDates); const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery(undefined, { - enabled: !!props.open || !!searchParams.get("overlayCalendar"), + enabled: !!props.open || Boolean(searchParams?.get("overlayCalendar")), }); const { toggleValue, hasItem, set } = useLocalSet<{ credentialId: number; diff --git a/packages/features/ee/payments/components/Payment.tsx b/packages/features/ee/payments/components/Payment.tsx index 4311e9036c..dc575e6320 100644 --- a/packages/features/ee/payments/components/Payment.tsx +++ b/packages/features/ee/payments/components/Payment.tsx @@ -94,7 +94,7 @@ const PaymentForm = (props: Props) => { location?: string; } = { uid: props.booking.uid, - email: searchParams.get("email"), + email: searchParams?.get("email"), }; if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) { payload = await stripe.confirmSetup({ diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 69df6baaf6..2a39590dc9 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -61,7 +61,7 @@ function useRouterHelpers() { const pathname = usePathname(); const goto = (newSearchParams: Record) => { - const newQuery = new URLSearchParams(searchParams); + const newQuery = new URLSearchParams(searchParams ?? undefined); Object.keys(newSearchParams).forEach((key) => { newQuery.set(key, newSearchParams[key]); }); @@ -70,7 +70,7 @@ function useRouterHelpers() { }; const removeQueryParams = (queryParams: string[]) => { - const params = new URLSearchParams(searchParams); + const params = new URLSearchParams(searchParams ?? undefined); queryParams.forEach((param) => { params.delete(param); @@ -529,7 +529,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({ ); const s = (href: string) => { - const _searchParams = new URLSearchParams(searchParams); + const _searchParams = new URLSearchParams(searchParams ?? undefined); const [a, b] = href.split("="); _searchParams.set(a, b); return `${pathname?.split("?")[0] ?? ""}?${_searchParams.toString()}`; diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 9b3aa85be2..7f096a1e30 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -2,13 +2,13 @@ import type { AppCategories, Prisma } from "@prisma/client"; import appStore from "@calcom/app-store"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; -import type { EventTypeModel } from "@calcom/prisma/zod"; +import type { CompleteEventType } from "@calcom/prisma/zod"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; const handlePayment = async ( evt: CalendarEvent, - selectedEventType: Pick, "metadata" | "title">, + selectedEventType: Pick, paymentAppCredentials: { key: Prisma.JsonValue; appId: EventTypeAppsList; From 79c1aa60a2676ce5d1d96df1c98a7c6a30932cde Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:34:27 +0300 Subject: [PATCH 050/118] perf: database index on booking_status_starttime_endtime (#12066) --- .../migration.sql | 2 ++ packages/prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 packages/prisma/migrations/20231024173642_idx_booking_status_starttime_endtime/migration.sql diff --git a/packages/prisma/migrations/20231024173642_idx_booking_status_starttime_endtime/migration.sql b/packages/prisma/migrations/20231024173642_idx_booking_status_starttime_endtime/migration.sql new file mode 100644 index 0000000000..8ac1699440 --- /dev/null +++ b/packages/prisma/migrations/20231024173642_idx_booking_status_starttime_endtime/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "Booking_startTime_endTime_status_idx" ON "Booking"("startTime", "endTime", "status"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 78c0ce069c..7eee884084 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -423,6 +423,7 @@ model Booking { @@index([recurringEventId]) @@index([uid]) @@index([status]) + @@index([startTime, endTime, status]) } model Schedule { From af801df421ed210987a577c954ecf8cffbc9bf97 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Wed, 25 Oct 2023 15:57:29 +0530 Subject: [PATCH 051/118] Fixes in teams and avatar across org (#12070) --- apps/web/pages/event-types/index.tsx | 17 ++------ packages/lib/getEventTypeById.ts | 14 ++----- .../viewer/eventTypes/getByViewer.handler.ts | 1 + .../routers/viewer/teams/list.handler.ts | 39 ++----------------- 4 files changed, 12 insertions(+), 59 deletions(-) diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index bd87521314..4d68795e92 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -72,6 +72,7 @@ import useMeQuery from "@lib/hooks/useMeQuery"; import PageWrapper from "@components/PageWrapper"; import SkeletonLoader from "@components/eventtype/SkeletonLoader"; +import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup"; type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; type EventTypeGroupProfile = EventTypeGroups[number]["profile"]; @@ -398,23 +399,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
    {type.team && !isManagedEventType && ( - ({ - alt: organizer.name || "", - image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${ - organizer.username - }/avatar.png`, - title: organizer.name || "", - }) - ) - : [] - } + users={type?.users ?? []} /> )} {isManagedEventType && type?.children && type.children?.length > 0 && ( diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 7637446d80..7ce86719fd 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -4,10 +4,9 @@ import { getLocationGroupedOptions } from "@calcom/app-store/server"; import type { StripeData } from "@calcom/app-store/stripepayment/lib/server"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import type { LocationObject } from "@calcom/core/location"; -import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@calcom/lib"; -import { CAL_URL } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { PrismaClient } from "@calcom/prisma"; import type { Credential } from "@calcom/prisma/client"; @@ -36,6 +35,7 @@ export default async function getEventTypeById({ username: true, id: true, email: true, + organizationId: true, locale: true, defaultScheduleId: true, }); @@ -298,9 +298,7 @@ export default async function getEventTypeById({ const eventTypeUsers: ((typeof eventType.users)[number] & { avatar: string })[] = eventType.users.map( (user) => ({ ...user, - avatar: `${eventType.team?.parent?.slug ? getOrgFullOrigin(eventType.team?.parent?.slug) : CAL_URL}/${ - user.username - }/avatar.png`, + avatar: getUserAvatarUrl(user), }) ); @@ -346,11 +344,7 @@ export default async function getEventTypeById({ .map((member) => { const user: typeof member.user & { avatar: string } = { ...member.user, - avatar: `${ - eventTypeObject.team?.parent?.slug - ? getOrgFullOrigin(eventTypeObject.team?.parent?.slug) - : CAL_URL - }/${member.user.username}/avatar.png`, + avatar: getUserAvatarUrl(member.user), }; return { ...user, diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index 6c154cdb30..83f8089c88 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -30,6 +30,7 @@ const userSelect = Prisma.validator()({ id: true, username: true, name: true, + organizationId: true, }); const userEventTypeSelect = Prisma.validator()({ diff --git a/packages/trpc/server/routers/viewer/teams/list.handler.ts b/packages/trpc/server/routers/viewer/teams/list.handler.ts index 4aa8947e19..44220804aa 100644 --- a/packages/trpc/server/routers/viewer/teams/list.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/list.handler.ts @@ -1,4 +1,3 @@ -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; import { prisma } from "@calcom/prisma"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -11,42 +10,12 @@ type ListOptions = { }; export const listHandler = async ({ ctx }: ListOptions) => { - if (ctx.user?.organization?.id) { - const membershipsWithoutParent = await prisma.membership.findMany({ - where: { - userId: ctx.user.id, - team: { - parent: { - is: { - id: ctx.user?.organization?.id, - }, - }, - }, - }, - include: { - team: { - include: { - inviteTokens: true, - }, - }, - }, - orderBy: { role: "desc" }, - }); - - const isOrgAdmin = !!(await isOrganisationAdmin(ctx.user.id, ctx.user.organization.id)); // Org id exists here as we're inside a conditional TS complaining for some reason - - return membershipsWithoutParent.map(({ team: { inviteTokens, ..._team }, ...membership }) => ({ - role: membership.role, - accepted: membership.accepted, - isOrgAdmin, - ..._team, - /** To prevent breaking we only return non-email attached token here, if we have one */ - inviteToken: inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${_team.id}`), - })); - } - const memberships = await prisma.membership.findMany({ where: { + // Show all the teams this user belongs to regardless of the team being part of the user's org or not + // We don't want to restrict in the listing here. If we need to restrict a situation where a user is part of the org along with being part of a non-org team, we should do that instead of filtering out from here + // This became necessary when we started migrating user to Org, without migrating some teams of the user to the org + // Also, we would allow a user to be part of multiple orgs, then also it would be necessary. userId: ctx.user.id, }, include: { From 327159c2ae450eb8090e19d42ba9d057937a44e4 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:41:25 +0100 Subject: [PATCH 052/118] fix/profile-dont-wait-for-avatar (#12080) * fix/profile-dont-wait-for-avatar * Update apps/web/pages/settings/my-account/profile.tsx * Update apps/web/pages/settings/my-account/profile.tsx --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- apps/web/pages/settings/my-account/profile.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index bfe82cdc27..7b1d873dd8 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -78,8 +78,8 @@ type FormValues = { bio: string; }; -const checkIfItFallbackImage = (fetchedImgSrc: string) => { - return fetchedImgSrc.endsWith(AVATAR_FALLBACK); +const checkIfItFallbackImage = (fetchedImgSrc?: string) => { + return !fetchedImgSrc || fetchedImgSrc.endsWith(AVATAR_FALLBACK); }; const ProfileView = () => { @@ -226,10 +226,11 @@ const ProfileView = () => { [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), }; - if (isLoading || !user || fetchedImgSrc === undefined) + if (isLoading || !user) { return ( ); + } const defaultValues = { username: user.username || "", From efc7be0b6bb01f5ca3bd68b7bfd4230a4be510bd Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 25 Oct 2023 14:18:25 +0100 Subject: [PATCH 053/118] fix: Infinite loop in timezones on the negative side of UTC (#12063) * fix: Infinite loop in timezones on the negative side of UTC * Update packages/features/calendars/lib/getAvailableDatesInMonth.test.ts * Revert back to real system time after test * Handle all dates as local time, given this all happens in the browser --- .github/workflows/test.yml | 2 ++ ...getAvailableDatesInMonth.timezone.test.ts} | 32 +++++++++++++++++-- .../calendars/lib/getAvailableDatesInMonth.ts | 8 +++-- vitest.workspace.ts | 21 ++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) rename packages/features/calendars/lib/{getAvailableDatesInMonth.test.ts => getAvailableDatesInMonth.timezone.test.ts} (53%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fcc7c6ed1f..d8ca18d282 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,3 +15,5 @@ jobs: - uses: ./.github/actions/yarn-install # Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners - run: yarn test + # We could add different timezones here that we need to run our tests in + - run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts similarity index 53% rename from packages/features/calendars/lib/getAvailableDatesInMonth.test.ts rename to packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts index 10e8fdc147..c8475e3311 100644 --- a/packages/features/calendars/lib/getAvailableDatesInMonth.test.ts +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.timezone.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; @@ -8,7 +8,7 @@ describe("Test Suite: Date Picker", () => { // *) Use right amount of days in given month. (28, 30, 31) test("it returns the right amount of days in a given month", () => { const currentDate = new Date(); - const nextMonthDate = new Date(Date.UTC(currentDate.getFullYear(), currentDate.getMonth() + 1)); + const nextMonthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1); const result = getAvailableDatesInMonth({ browsingDate: nextMonthDate, @@ -35,5 +35,33 @@ describe("Test Suite: Date Picker", () => { expect(result).toHaveLength(1); }); + + test("it translates correctly regardless of system time", () => { + { + // test a date in negative UTC offset + vi.useFakeTimers().setSystemTime(new Date("2023-10-24T13:27:00.000-07:00")); + + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + }); + + expect(result).toHaveLength(daysInMonth(currentDate) - currentDate.getDate() + 1); + } + { + // test a date in positive UTC offset + vi.useFakeTimers().setSystemTime(new Date("2023-10-24T13:27:00.000+07:00")); + + const currentDate = new Date(); + const result = getAvailableDatesInMonth({ + browsingDate: currentDate, + }); + + expect(result).toHaveLength(daysInMonth(currentDate) - currentDate.getDate() + 1); + } + // Undo the forced time we applied earlier, reset to system default. + vi.setSystemTime(vi.getRealSystemTime()); + vi.useRealTimers(); + }); }); }); diff --git a/packages/features/calendars/lib/getAvailableDatesInMonth.ts b/packages/features/calendars/lib/getAvailableDatesInMonth.ts index 8fbace876b..8e50ef9793 100644 --- a/packages/features/calendars/lib/getAvailableDatesInMonth.ts +++ b/packages/features/calendars/lib/getAvailableDatesInMonth.ts @@ -5,7 +5,7 @@ import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; // *) Dates in the past are not available. // *) Use right amount of days in given month. (28, 30, 31) export function getAvailableDatesInMonth({ - browsingDate, // pass as UTC + browsingDate, minDate = new Date(), includedDates, }: { @@ -15,12 +15,14 @@ export function getAvailableDatesInMonth({ }) { const dates = []; const lastDateOfMonth = new Date( - Date.UTC(browsingDate.getFullYear(), browsingDate.getMonth(), daysInMonth(browsingDate)) + browsingDate.getFullYear(), + browsingDate.getMonth(), + daysInMonth(browsingDate) ); for ( let date = browsingDate > minDate ? browsingDate : minDate; date <= lastDateOfMonth; - date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 1)) + date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) ) { // intersect included dates if (includedDates && !includedDates.includes(yyyymmdd(date))) { diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 587aeb8cbf..20d12799fb 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,6 +1,13 @@ import { defineWorkspace } from "vitest/config"; const packagedEmbedTestsOnly = process.argv.includes("--packaged-embed-tests-only"); +const timeZoneDependentTestsOnly = process.argv.includes("--timeZoneDependentTestsOnly"); +// eslint-disable-next-line turbo/no-undeclared-env-vars +const envTZ = process.env.TZ; +if (timeZoneDependentTestsOnly && !envTZ) { + throw new Error("TZ environment variable is not set"); +} + // defineWorkspace provides a nice type hinting DX const workspaces = packagedEmbedTestsOnly ? [ @@ -11,6 +18,19 @@ const workspaces = packagedEmbedTestsOnly }, }, ] + : // It doesn't seem to be possible to fake timezone per test, so we rerun the entire suite with different TZ. See https://github.com/vitest-dev/vitest/issues/1575#issuecomment-1439286286 + timeZoneDependentTestsOnly + ? [ + { + test: { + name: `TimezoneDependentTests:${envTZ}`, + include: ["packages/**/*.timezone.test.ts", "apps/**/*.timezone.test.ts"], + // TODO: Ignore the api until tests are fixed + exclude: ["**/node_modules/**/*", "packages/embeds/**/*"], + setupFiles: ["setupVitest.ts"], + }, + }, + ] : [ { test: { @@ -20,6 +40,7 @@ const workspaces = packagedEmbedTestsOnly setupFiles: ["setupVitest.ts"], }, }, + { test: { name: "@calcom/closecom", From 1929b23ea83b81ee102c016dfa5a4b554025d496 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:21:14 +0400 Subject: [PATCH 054/118] Update handleNewBooking.ts (#12081) --- packages/features/bookings/lib/handleNewBooking.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 83886bab35..fa810f1bf8 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -2378,6 +2378,7 @@ async function handler( ...eventTypeInfo, bookingId: booking?.id, rescheduleUid, + oldBookingId: originalRescheduledBooking?.id || undefined, rescheduleStartTime: originalRescheduledBooking?.startTime ? dayjs(originalRescheduledBooking?.startTime).utc().format() : undefined, From 93640552837a0b3fd96cd087b09b29bb9f514f2d Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:26:22 +0400 Subject: [PATCH 055/118] fix: typefix for webhook and rename oldBookingid to rescheduleId (#12084) * add rescheduleId to type * update oldBookingId to rescheduleId * Remove old remnant --- packages/features/bookings/lib/handleNewBooking.ts | 3 ++- packages/features/webhooks/lib/sendPayload.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index fa810f1bf8..3488442086 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1851,6 +1851,7 @@ async function handler( ...eventTypeInfo, uid: resultBooking?.uid || uid, bookingId: booking?.id, + rescheduleId: originalRescheduledBooking?.id || undefined, rescheduleUid, rescheduleStartTime: originalRescheduledBooking?.startTime ? dayjs(originalRescheduledBooking?.startTime).utc().format() @@ -2377,8 +2378,8 @@ async function handler( ...evt, ...eventTypeInfo, bookingId: booking?.id, + rescheduleId: originalRescheduledBooking?.id || undefined, rescheduleUid, - oldBookingId: originalRescheduledBooking?.id || undefined, rescheduleStartTime: originalRescheduledBooking?.startTime ? dayjs(originalRescheduledBooking?.startTime).utc().format() : undefined, diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index e0211a6a71..7ff22d9577 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -22,6 +22,7 @@ export type WebhookDataType = CalendarEvent & bookingId?: number; status?: string; smsReminderNumber?: string; + rescheduleId?: number; rescheduleUid?: string; rescheduleStartTime?: string; rescheduleEndTime?: string; From 0fb75b715dabc337d12fc603a293d51f1f10129b Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Wed, 25 Oct 2023 22:59:41 +0530 Subject: [PATCH 056/118] fix: event type invalidation (#12077) --- .../features/eventtypes/components/CreateEventTypeDialog.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx index 7032079c13..6fd6483d65 100644 --- a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx @@ -79,6 +79,7 @@ export default function CreateEventTypeDialog({ membershipRole: MembershipRole | null | undefined; }[]; }) { + const utils = trpc.useContext(); const { t } = useLocale(); const router = useRouter(); const [firstRender, setFirstRender] = useState(true); @@ -116,6 +117,7 @@ export default function CreateEventTypeDialog({ const createMutation = trpc.viewer.eventTypes.create.useMutation({ onSuccess: async ({ eventType }) => { + await utils.viewer.eventTypes.getByViewer.invalidate(); await router.replace(`/event-types/${eventType.id}`); showToast( t("event_type_created_successfully", { From 1c65f5c150a964da4d32106dd77bbbf21e96fa65 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 25 Oct 2023 19:00:14 +0100 Subject: [PATCH 057/118] v3.4.4 --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 829f47e542..429b56feb3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.4.3", + "version": "3.4.4", "private": true, "scripts": { "analyze": "ANALYZE=true next build", From f9ad99e5728e317739c0b744696f6925c0b0ed75 Mon Sep 17 00:00:00 2001 From: Siddharth Movaliya Date: Wed, 25 Oct 2023 23:46:01 +0530 Subject: [PATCH 058/118] feat: Lock timezone on booking page (#11891) Co-authored-by: CarinaWolli --- .../components/eventtype/EventAdvancedTab.tsx | 17 ++++++++ apps/web/pages/[user].tsx | 1 + apps/web/pages/event-types/[type]/index.tsx | 1 + apps/web/public/static/locales/en/common.json | 2 + .../test/lib/handleChildrenEventTypes.test.ts | 41 +++++++++++++------ .../bookings/Booker/components/EventMeta.tsx | 7 +++- .../features/bookings/lib/handleNewBooking.ts | 2 + .../features/eventtypes/lib/getPublicEvent.ts | 1 + packages/lib/defaultEvents.ts | 1 + packages/lib/getEventTypeById.ts | 1 + packages/lib/test/builder.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + packages/prisma/selects/event-types.ts | 2 + 14 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 packages/prisma/migrations/20231020090443_add_lock_timezone_toggle/migration.sql diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 563250a671..6ddba9b78b 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -433,6 +433,23 @@ export const EventAdvancedTab = ({ eventType, team }: Pick )} /> + ( + onChange(e)} + /> + )} + /> {allowDisablingAttendeeConfirmationEmails(workflows) && ( <> { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line - const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } = - mockFindFirstEventType({ - id: 123, - metadata: { managedEventConfig: {} }, - locations: [], - }); + const { + schedulingType, + id, + teamId, + timeZone, + requiresBookerEmailVerification, + lockTimeZoneToggleOnBookingPage, + ...evType + } = mockFindFirstEventType({ + id: 123, + metadata: { managedEventConfig: {} }, + locations: [], + }); const result = await updateChildrenEventTypes({ eventTypeId: 1, oldEventType: { children: [], team: { name: "" } }, @@ -145,6 +152,7 @@ describe("handleChildrenEventTypes", () => { userId, scheduleId, requiresBookerEmailVerification, + lockTimeZoneToggleOnBookingPage, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -230,12 +238,19 @@ describe("handleChildrenEventTypes", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore // eslint-disable-next-line - const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } = - mockFindFirstEventType({ - id: 123, - metadata: { managedEventConfig: {} }, - locations: [], - }); + const { + schedulingType, + id, + teamId, + timeZone, + requiresBookerEmailVerification, + lockTimeZoneToggleOnBookingPage, + ...evType + } = mockFindFirstEventType({ + id: 123, + metadata: { managedEventConfig: {} }, + locations: [], + }); prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload); const result = await updateChildrenEventTypes({ eventTypeId: 1, @@ -277,6 +292,7 @@ describe("handleChildrenEventTypes", () => { parentId, userId, requiresBookerEmailVerification, + lockTimeZoneToggleOnBookingPage, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -327,6 +343,7 @@ describe("handleChildrenEventTypes", () => { userId: _userId, // eslint-disable-next-line @typescript-eslint/no-unused-vars requiresBookerEmailVerification, + lockTimeZoneToggleOnBookingPage, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 83bcfc6312..f282e48d5e 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -105,6 +105,7 @@ export const EventMeta = () => { )} + { {bookerState === "booking" ? ( <>{timezone} ) : ( - + { }} value={timezone} onChange={(tz) => setTimezone(tz.value)} + isDisabled={event.lockTimeZoneToggleOnBookingPage} /> )} diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 3488442086..997b318bf4 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -276,6 +276,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => { periodEndDate: true, periodDays: true, periodCountCalendarDays: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, userId: true, @@ -2686,6 +2687,7 @@ const findBookingQuery = async (bookingId: number) => { description: true, currency: true, length: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, price: true, diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 3b6d8f704f..85bab2e26f 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -33,6 +33,7 @@ const publicEventSelect = Prisma.validator()({ customInputs: true, disableGuests: true, metadata: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, recurringEvent: true, diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 5aba14feaa..5243b63ebd 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -86,6 +86,7 @@ const commons = { recurringEvent: null, destinationCalendar: null, team: null, + lockTimeZoneToggleOnBookingPage: false, requiresConfirmation: false, requiresBookerEmailVerification: false, bookingLimits: null, diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 7ce86719fd..be26508d9f 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -89,6 +89,7 @@ export default async function getEventTypeById({ periodStartDate: true, periodEndDate: true, periodCountCalendarDays: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, recurringEvent: true, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 7769023a59..bd9e9fc516 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -85,6 +85,7 @@ export const buildEventType = (eventType?: Partial): EventType => { periodDays: null, periodCountCalendarDays: null, recurringEvent: null, + lockTimeZoneToggleOnBookingPage: false, requiresConfirmation: false, disableGuests: false, hideCalendarNotes: false, diff --git a/packages/prisma/migrations/20231020090443_add_lock_timezone_toggle/migration.sql b/packages/prisma/migrations/20231020090443_add_lock_timezone_toggle/migration.sql new file mode 100644 index 0000000000..c97b6146c5 --- /dev/null +++ b/packages/prisma/migrations/20231020090443_add_lock_timezone_toggle/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "lockTimeZoneToggleOnBookingPage" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 7eee884084..6356442fa9 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -86,6 +86,7 @@ model EventType { periodEndDate DateTime? periodDays Int? periodCountCalendarDays Boolean? + lockTimeZoneToggleOnBookingPage Boolean @default(false) requiresConfirmation Boolean @default(false) requiresBookerEmailVerification Boolean @default(false) /// @zod.custom(imports.recurringEventType) diff --git a/packages/prisma/selects/event-types.ts b/packages/prisma/selects/event-types.ts index c47e148dde..35733ede28 100644 --- a/packages/prisma/selects/event-types.ts +++ b/packages/prisma/selects/event-types.ts @@ -11,6 +11,7 @@ export const baseEventTypeSelect = Prisma.validator()({ hidden: true, price: true, currency: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, }); @@ -28,6 +29,7 @@ export const bookEventTypeSelect = Prisma.validator()({ periodStartDate: true, periodEndDate: true, recurringEvent: true, + lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, requiresBookerEmailVerification: true, metadata: true, From 158da51a5d9b3de3dcaa7210f1a77f495515ad06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 25 Oct 2023 12:33:22 -0700 Subject: [PATCH 059/118] fix: embed rewrites post dotted usernames (#12087) --- apps/web/next.config.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 22da1946e5..dc4cbdafa2 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -226,6 +226,14 @@ const nextConfig = { }, async rewrites() { const beforeFiles = [ + { + /** + * Needed due to the introduction of dotted usernames + * @see https://github.com/calcom/cal.com/pull/11706 + */ + source: "/embed.js", + destination: "/embed/embed.js", + }, { source: "/login", destination: "/auth/login", From 139a7c8249b82fe86f92d9a1bf2c27f26ee59516 Mon Sep 17 00:00:00 2001 From: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Date: Wed, 25 Oct 2023 23:58:08 +0300 Subject: [PATCH 060/118] chore: [app dir bootstrapping 5] add RootLayout (#11982) Co-authored-by: zomars Co-authored-by: Greg Pabian <35925521+grzpab@users.noreply.github.com> --- ...honenumber-js-npm-1.10.12-51c84f8bf1.patch | 15 + apps/web/app/layout.tsx | 109 +++++++ apps/web/components/PageWrapperAppDir.tsx | 88 ++++++ apps/web/lib/app-providers-app-dir.tsx | 291 ++++++++++++++++++ package.json | 3 +- yarn.lock | 15 +- 6 files changed, 516 insertions(+), 5 deletions(-) create mode 100644 .yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch create mode 100644 apps/web/app/layout.tsx create mode 100644 apps/web/components/PageWrapperAppDir.tsx create mode 100644 apps/web/lib/app-providers-app-dir.tsx diff --git a/.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch b/.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch new file mode 100644 index 0000000000..7cfa242404 --- /dev/null +++ b/.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch @@ -0,0 +1,15 @@ +diff --git a/index.cjs b/index.cjs +index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644 +--- a/index.cjs ++++ b/index.cjs +@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) { + // https://github.com/babel/babel/issues/2212#issuecomment-131827986 + // An alternative approach: + // https://www.npmjs.com/package/babel-plugin-add-module-exports +-exports = module.exports = min.parsePhoneNumberFromString +-exports['default'] = min.parsePhoneNumberFromString ++// exports = module.exports = min.parsePhoneNumberFromString ++// exports['default'] = min.parsePhoneNumberFromString + + // `parsePhoneNumberFromString()` named export is now considered legacy: + // it has been promoted to a default export due to being too verbose. diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 0000000000..23d224b6e1 --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,109 @@ +import type { Metadata } from "next"; +import { headers as nextHeaders, cookies as nextCookies } from "next/headers"; +import Script from "next/script"; +import React from "react"; + +import { getLocale } from "@calcom/features/auth/lib/getLocale"; +import { IS_PRODUCTION } from "@calcom/lib/constants"; + +import "../styles/globals.css"; + +export const metadata: Metadata = { + icons: { + icon: [ + { + sizes: "32x32", + url: "/api/logo?type=favicon-32", + }, + { + sizes: "16x16", + url: "/api/logo?type=favicon-16", + }, + ], + apple: { + sizes: "180x180", + url: "/api/logo?type=apple-touch-icon", + }, + other: [ + { + url: "/safari-pinned-tab.svg", + rel: "mask-icon", + }, + ], + }, + manifest: "/site.webmanifest", + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#f9fafb" }, + { media: "(prefers-color-scheme: dark)", color: "#1C1C1C" }, + ], + other: { + "msapplication-TileColor": "#000000", + }, +}; + +const getInitialProps = async ( + url: string, + headers: ReturnType, + cookies: ReturnType +) => { + const { pathname, searchParams } = new URL(url); + + const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null; + const embedColorScheme = searchParams?.get("ui.color-scheme"); + + // @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale + const newLocale = await getLocale({ headers, cookies }); + let direction = "ltr"; + + try { + const intlLocale = new Intl.Locale(newLocale); + // @ts-expect-error INFO: Typescript does not know about the Intl.Locale textInfo attribute + direction = intlLocale.textInfo?.direction; + } catch (e) { + console.error(e); + } + + return { isEmbed, embedColorScheme, locale: newLocale, direction }; +}; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const headers = nextHeaders(); + const cookies = nextCookies(); + + const fullUrl = headers.get("x-url") ?? ""; + const nonce = headers.get("x-csp") ?? ""; + + const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies); + return ( + + + {!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && ( + // eslint-disable-next-line @next/next/no-sync-scripts +