Compare commits
@@ -7,7 +7,6 @@ public
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
*.test.ts
|
||||
|
||||
.gitignore
|
||||
.npmignore
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
public/embed
|
||||
*.test.ts
|
||||
public/embed
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.9",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
const selectedCalendarSelectSchema = z.object({
|
||||
integration: z.string(),
|
||||
@@ -25,7 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
timeZone: true,
|
||||
id: true,
|
||||
selectedCalendars: true,
|
||||
|
||||
@@ -52,8 +52,6 @@ export default function Login({
|
||||
totpEmail,
|
||||
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const isTeamInvite = searchParams.get("teamInvite");
|
||||
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const formSchema = z
|
||||
@@ -97,9 +95,7 @@ export default function Login({
|
||||
callbackUrl = safeCallbackUrl || "";
|
||||
|
||||
const LoginFooter = (
|
||||
<a
|
||||
href={callbackUrl !== "" ? `${WEBSITE_URL}/signup?callbackUrl=${callbackUrl}` : `${WEBSITE_URL}/signup`}
|
||||
className="text-brand-500 font-medium">
|
||||
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
|
||||
{t("dont_have_an_account")}
|
||||
</a>
|
||||
);
|
||||
@@ -188,9 +184,6 @@ export default function Login({
|
||||
? LoginFooter
|
||||
: null
|
||||
}>
|
||||
{isTeamInvite && (
|
||||
<Alert severity="info" message={t("signin_or_signup_to_accept_invite")} className="mb-4 mt-4" />
|
||||
)}
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
|
||||
<div>
|
||||
|
||||
@@ -7,13 +7,8 @@ import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/li
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import {
|
||||
hostedCal,
|
||||
isSAMLLoginEnabled,
|
||||
samlProductID,
|
||||
samlTenantID,
|
||||
samlTenantProduct,
|
||||
} from "@calcom/features/ee/sso/lib/saml";
|
||||
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -111,7 +106,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
error = "Email not provided";
|
||||
} else {
|
||||
try {
|
||||
const ret = await samlTenantProduct(prisma, emailParam);
|
||||
const ret = await ssoTenantProduct(prisma, emailParam);
|
||||
tenant = ret.tenant;
|
||||
product = ret.product;
|
||||
} catch (e) {
|
||||
|
||||
@@ -461,7 +461,7 @@ export default function Success(props: SuccessProps) {
|
||||
{locationToDisplay && !isCancelled && (
|
||||
<>
|
||||
<div className="mt-3 font-medium">{t("where")}</div>
|
||||
<div className="col-span-2 mt-3">
|
||||
<div className="col-span-2 mt-3" data-testid="where">
|
||||
{locationToDisplay.startsWith("http") ? (
|
||||
<a
|
||||
href={locationToDisplay}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function InsightsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ShellMain hideHeadingOnMobile>
|
||||
<ShellMain heading="Insights" subtitle={t("insights_subtitle")}>
|
||||
<UpgradeTip
|
||||
title={t("make_informed_decisions")}
|
||||
description={t("make_informed_decisions_description")}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
@@ -35,23 +34,6 @@ type FormValues = z.infer<typeof signupSchema>;
|
||||
|
||||
type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
const getSafeCallbackUrl = (url: string | null) => {
|
||||
if (!url) return null;
|
||||
|
||||
let callbackUrl = url;
|
||||
|
||||
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
|
||||
|
||||
// If not absolute URL, make it absolute
|
||||
if (!/^https?:\/\//.test(callbackUrl)) {
|
||||
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
|
||||
}
|
||||
|
||||
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
|
||||
|
||||
return safeCallbackUrl;
|
||||
};
|
||||
|
||||
export default function Signup({ prepopulateFormValues, token, orgSlug }: SignupProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -73,7 +55,6 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
throw new Error(err.message);
|
||||
}
|
||||
};
|
||||
const callbackUrl = getSafeCallbackUrl(searchParams.get("callbackUrl"));
|
||||
|
||||
const signUp: SubmitHandler<FormValues> = async (data) => {
|
||||
await fetch("/api/auth/signup", {
|
||||
@@ -91,10 +72,13 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
.then(async () => {
|
||||
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
||||
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
|
||||
|
||||
await signIn<"credentials">("credentials", {
|
||||
...data,
|
||||
callbackUrl: `${callbackUrl ? callbackUrl : `${WEBAPP_URL}/${verifyOrGettingStarted}`}?from=signup`,
|
||||
callbackUrl: `${
|
||||
searchParams?.get("callbackUrl")
|
||||
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
|
||||
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
|
||||
}?from=signup`,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -173,7 +157,9 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||
className="w-full justify-center"
|
||||
onClick={() =>
|
||||
signIn("Cal.com", {
|
||||
callbackUrl: callbackUrl ? callbackUrl : `${WEBAPP_URL}/getting-started`,
|
||||
callbackUrl: searchParams?.get("callbackUrl")
|
||||
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
|
||||
: `${WEBAPP_URL}/getting-started`,
|
||||
})
|
||||
}>
|
||||
{t("login_instead")}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
@@ -43,21 +41,6 @@ function Teams() {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
await ssr.viewer.me.prefetch();
|
||||
const session = await getServerSession({ req: context.req, res: context.res });
|
||||
const token = context.query?.token;
|
||||
const resolvedUrl = context.resolvedUrl;
|
||||
|
||||
const callbackUrl = token ? getSafeRedirectUrl(`${WEBAPP_URL}${resolvedUrl}`) : null;
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}&teamInvite=true` : "/auth/login",
|
||||
permanent: false,
|
||||
},
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate() } };
|
||||
};
|
||||
|
||||
@@ -142,17 +142,25 @@ test.describe("Event Types tests", () => {
|
||||
.first()
|
||||
.getAttribute("href");
|
||||
|
||||
/**
|
||||
* Verify first organizer address
|
||||
*/
|
||||
await page.goto(previewLink ?? "");
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
for (const location of locationData) {
|
||||
await page.locator(`span:has-text("${location}")`).click();
|
||||
}
|
||||
|
||||
await page.locator(`span:has-text("${locationData[0]}")`).click();
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
await expect(page.locator(`[data-testid="where"]`)).toHaveText(locationData[0]);
|
||||
|
||||
/**
|
||||
* Verify second organizer address
|
||||
*/
|
||||
await page.goto(previewLink ?? "");
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await page.locator(`span:has-text("${locationData[1]}")`).click();
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
await expect(page.locator(`[data-testid="where"]`)).toHaveText(locationData[1]);
|
||||
});
|
||||
|
||||
test.describe("Different Locations Tests", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { authenticator } from "otplib";
|
||||
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
@@ -61,7 +62,7 @@ test.describe("2FA Tests", async () => {
|
||||
|
||||
await test.step("Login with 2FA enabled", async () => {
|
||||
await user.login();
|
||||
const userWith2FaSecret = await prisma?.user.findFirst({
|
||||
const userWith2FaSecret = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
@@ -138,7 +139,7 @@ test.describe("2FA Tests", async () => {
|
||||
await page.click(`[data-testid=two-factor-switch][data-state="checked"]`);
|
||||
await page.fill('input[name="password"]', userPassword);
|
||||
|
||||
const userWith2FaSecret = await prisma?.user.findFirst({
|
||||
const userWith2FaSecret = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"team_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Teamplan getestet haben. Wir haben festgestellt, dass Ihr Team „{{teamName}}“ aktualisiert werden muss.",
|
||||
"upgrade_banner_action": "Hier aufrüsten",
|
||||
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
|
||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
|
||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Tarif „Organization“ getestet haben. Wir haben festgestellt, dass Ihre Organization „{{teamName}}“ ein Upgrade benötigt.",
|
||||
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
|
||||
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
|
||||
"hey_there": "Hallo,",
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"invite_team_individual_segment": "Invite individual",
|
||||
"invite_team_bulk_segment": "Bulk import",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"signin_or_signup_to_accept_invite": "You need to Sign in or Sign up to see team invitation.",
|
||||
"your_event_has_been_scheduled": "Your event has been scheduled",
|
||||
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
|
||||
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||
@@ -2051,5 +2050,8 @@
|
||||
"no_members_found": "No members found",
|
||||
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
|
||||
"availability_schedules":"Availability Schedules",
|
||||
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
|
||||
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
|
||||
"edit_users_availability":"Edit user's availability: {{username}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"invite_team_individual_segment": "Inviter une personne",
|
||||
"invite_team_bulk_segment": "Importation multiple",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"signin_or_signup_to_accept_invite": "Vous devez vous connecter ou vous inscrire pour voir l'invitation d'équipe.",
|
||||
"your_event_has_been_scheduled": "Votre événement a été planifié",
|
||||
"your_event_has_been_scheduled_recurring": "Votre événement récurrent a été planifié",
|
||||
"accept_our_license": "Acceptez notre licence en changeant la variable .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> en « {{agree}} ».",
|
||||
|
||||
@@ -17,9 +17,15 @@
|
||||
"verify_email_banner_body": "Verifiera din e-postadress för att garantera bästa e-post- och kalenderleverans",
|
||||
"verify_email_email_header": "Verifiera din e-postadress",
|
||||
"verify_email_email_button": "Verifiera e-post",
|
||||
"copy_somewhere_safe": "Spara denna API-nyckel någonstans säkert. Du kommer inte att kunna visa den igen.",
|
||||
"verify_email_email_body": "Verifiera din e-postadress genom att klicka på knappen nedan.",
|
||||
"verify_email_by_code_email_body": "Verifiera din e-postadress genom att använda nedanstående kod.",
|
||||
"verify_email_email_link_text": "Här är länken om du inte gillar att klicka på knappar:",
|
||||
"email_verification_code": "Ange verifieringskod",
|
||||
"email_verification_code_placeholder": "Ange verifieringskoden som skickats till din e-post",
|
||||
"incorrect_email_verification_code": "Vertifieringskoden är felaktig.",
|
||||
"email_sent": "E-postmeddelandet har skickats",
|
||||
"email_not_sent": "Ett fel uppstod när e-post skulle skickas",
|
||||
"event_declined_subject": "Avvisades: {{title}} kl. {{date}}",
|
||||
"event_cancelled_subject": "Avbokad: {{title}} {{date}}",
|
||||
"event_request_declined": "Din bokningsförfrågan har avbjöts",
|
||||
@@ -78,6 +84,7 @@
|
||||
"event_awaiting_approval_recurring": "En återkommande händelse väntar på ditt godkännande",
|
||||
"someone_requested_an_event": "Någon har begärt att schemalägga en händelse i din kalender.",
|
||||
"someone_requested_password_reset": "Någon har begärt en länk för att ändra ditt lösenord.",
|
||||
"password_reset_email_sent": "Om denna e-postadress finns i vårt system bör du få ett återställningsmeddelande.",
|
||||
"password_reset_instructions": "Om du inte begärde detta kan du ignorera detta e-postmeddelande och ditt lösenord kommer inte att ändras.",
|
||||
"event_awaiting_approval_subject": "Inväntar godkännande: {{title}} den {{date}}",
|
||||
"event_still_awaiting_approval": "En bokning väntar fortfarande på ditt godkännande",
|
||||
@@ -217,6 +224,10 @@
|
||||
"already_have_an_account": "Har du redan ett konto?",
|
||||
"create_account": "Skapa konto",
|
||||
"confirm_password": "Bekräfta lösenord",
|
||||
"confirm_auth_change": "Detta kommer att ändra hur du loggar in",
|
||||
"confirm_auth_email_change": "Att ändra e-postadressen kommer att koppla bort din nuvarande autentiseringsmetod för att logga in på Cal.com. Vi kommer att be dig verifiera din nya e-postadress. Framå kommer du att loggas ut och använda din nya e-postadress för att logga in istället för din nuvarande autentiseringsmetod efter att du ställt in ditt lösenord genom att följa instruktionerna som kommer att skickas till din e-post.",
|
||||
"reset_your_password": "Konfigurera ditt nya lösenord med instruktionerna som skickas till din e-postadress.",
|
||||
"email_change": "Logga in igen med din nya e-postadress och lösenord.",
|
||||
"create_your_account": "Skapa ditt konto",
|
||||
"sign_up": "Registrera dig",
|
||||
"youve_been_logged_out": "Du har loggats ut",
|
||||
@@ -243,6 +254,10 @@
|
||||
"all": "Alla",
|
||||
"yours": "Ditt konto",
|
||||
"available_apps": "Tillgängliga appar",
|
||||
"available_apps_lower_case": "Tillgängliga appar",
|
||||
"available_apps_desc": "Du har inga appar installerade. Visa populära appar nedan och utforska mer i vår <1>App Store</1>",
|
||||
"fixed_host_helper": "Lägg till alla som behöver delta i evenemanget. <1>Läs mer</1>",
|
||||
"round_robin_helper": "Personer i gruppen turas om och endast en person kommer att dyka upp för evenemanget.",
|
||||
"check_email_reset_password": "Kolla din e-post. Vi har skickat en länk till dig för att återställa ditt lösenord.",
|
||||
"finish": "Slutför",
|
||||
"organization_general_description": "Hantera inställningar för ditt teams språk och tidszon",
|
||||
@@ -251,6 +266,7 @@
|
||||
"nearly_there_instructions": "Till sist så hjälper en kort beskrivning om dig själv och ett foto dig verkligen att få bokningar och andra kan veta vem de bokar med.",
|
||||
"set_availability_instructions": "Definiera tidsintervall när du är tillgänglig på en återkommande basis. Du kan skapa fler av dessa senare och tilldela dem till olika kalendrar.",
|
||||
"set_availability": "Ställ in din tillgänglighet",
|
||||
"availability_settings": "Tillgänglighetsinställningar",
|
||||
"continue_without_calendar": "Fortsätt utan kalender",
|
||||
"connect_your_calendar": "Anslut din kalender",
|
||||
"connect_your_video_app": "Anslut dina videoappar",
|
||||
@@ -392,6 +408,8 @@
|
||||
"allow_dynamic_booking_tooltip": "Gruppbokningslänkar som kan skapas dynamiskt genom att lägga till flera användarnamn med ett '+'. Exempel: '{{appName}}/bailey+peer'",
|
||||
"allow_dynamic_booking": "Tillåt deltagare att boka dig via dynamiska gruppbokningar",
|
||||
"dynamic_booking": "Dynamiska grupplänkar",
|
||||
"allow_seo_indexing": "Tillåt sökmotorer att komma åt ditt publika innehåll",
|
||||
"seo_indexing": "Tillåt SEO-indexering",
|
||||
"email": "E-post",
|
||||
"email_placeholder": "jdoe@exempel.se",
|
||||
"full_name": "Fullständigt namn",
|
||||
@@ -406,6 +424,7 @@
|
||||
"booking_requested": "Bokning begärd",
|
||||
"meeting_ended": "Mötet avslutat",
|
||||
"form_submitted": "Formulär skickat",
|
||||
"booking_paid": "Bokning betald",
|
||||
"event_triggers": "Händelseutlösare",
|
||||
"subscriber_url": "Prenumerantens URL",
|
||||
"create_new_webhook": "Skapa en ny webhook",
|
||||
@@ -506,6 +525,7 @@
|
||||
"your_name": "Ditt namn",
|
||||
"your_full_name": "Ditt fullständiga namn",
|
||||
"no_name": "Inget namn",
|
||||
"enter_number_between_range": "Ange ett tal mellan 1 och {{maxOccurences}}",
|
||||
"email_address": "E-postadress",
|
||||
"enter_valid_email": "Ange en giltig e-postadress",
|
||||
"location": "Plats",
|
||||
@@ -543,6 +563,7 @@
|
||||
"leave": "Lämna",
|
||||
"profile": "Profil",
|
||||
"my_team_url": "Min team-URL",
|
||||
"my_teams": "Mina team",
|
||||
"team_name": "Teamnamn",
|
||||
"your_team_name": "Ditt team-namn",
|
||||
"team_updated_successfully": "Teamet har uppdaterats",
|
||||
@@ -570,6 +591,8 @@
|
||||
"invite_new_member": "Bjud in en ny teammedlem",
|
||||
"invite_new_member_description": "Obs! Detta kommer att <1>kosta en extra plats (150 kr/mån)</1> i din prenumeration.",
|
||||
"invite_new_team_member": "Bjud in någon till ditt team.",
|
||||
"upload_csv_file": "Ladda upp en .csv-fil",
|
||||
"invite_via_email": "Bjud in via e-post",
|
||||
"change_member_role": "Ändra teammedlemmens roll",
|
||||
"disable_cal_branding": "Inaktivera {{appName}}-varumärke",
|
||||
"disable_cal_branding_description": "Dölj all {{appName}} reklam från dina offentliga sidor.",
|
||||
@@ -842,6 +865,7 @@
|
||||
"team_view_user_availability_disabled": "Användaren måste acceptera inbjudan för att visa tillgängligheten",
|
||||
"set_as_away": "Ställ in dig själv som inte tillgänglig",
|
||||
"set_as_free": "Ställ av din status som inte tillgänglig",
|
||||
"toggle_away_error": "Fel vid uppdatering av frånvarostatus",
|
||||
"user_away": "Den här användaren är för inte tillgänglig.",
|
||||
"user_away_description": "Personen du försöker boka har satt sig som inte tillgänglig och accepterar därför inte nya bokningar.",
|
||||
"meet_people_with_the_same_tokens": "Träffa personer med samma tokens",
|
||||
@@ -851,6 +875,7 @@
|
||||
"account_managed_by_identity_provider_description": "Besök dina {{provider}} -kontoinställningar för att ändra din e-post, lösenord, aktivera tvåfaktorsautentisering mm.",
|
||||
"signin_with_google": "Logga in med Google",
|
||||
"signin_with_saml": "Logga in med SAML",
|
||||
"signin_with_saml_oidc": "Logga in med SAML/OIDC",
|
||||
"you_will_need_to_generate": "Du måste generera en åtkomsttoken från ditt gamla schemaläggningsverktyg.",
|
||||
"import": "Importera",
|
||||
"import_from": "Importera från",
|
||||
@@ -950,6 +975,8 @@
|
||||
"offer_seats_description": "Erbjud platser till bokningar. Detta inaktiverar gäster och bokningar.",
|
||||
"seats_available_one": "Tillgänglig plats",
|
||||
"seats_available_other": "Tillgängliga platser",
|
||||
"seats_nearly_full": "Sittplatser nästan fulla",
|
||||
"seats_half_full": "Sittplatser fylls snabbt",
|
||||
"number_of_seats": "Antal platser per bokning",
|
||||
"enter_number_of_seats": "Ange antal platser",
|
||||
"you_can_manage_your_schedules": "Du kan hantera dina scheman på sidan Tillgänglighet.",
|
||||
@@ -1030,12 +1057,15 @@
|
||||
"how_you_want_add_cal_site": "Hur vill du lägga till {{appName}} till din webbplats?",
|
||||
"choose_ways_put_cal_site": "Välj ett av följande sätt att lägga {{appName}} på din webbplats.",
|
||||
"setting_up_zapier": "Ställa in din Zapier-integration",
|
||||
"setting_up_make": "Konfigurera din Make-integration",
|
||||
"generate_api_key": "Generera API-nyckel",
|
||||
"generate_api_key_description": "Skapa en API-nyckel att använda med {{appName}} på",
|
||||
"your_unique_api_key": "Din unika API-nyckel",
|
||||
"copy_safe_api_key": "Kopiera denna API-nyckel och spara den någonstans säkert. Om du förlorar denna nyckel måste du skapa en ny.",
|
||||
"zapier_setup_instructions": "<0>Logga in på ditt Zapier-konto och skapa en ny Zap.</0><1>Välj {{appName}} som din Trigger-app. Välj också en Trigger-händelse. /1><2>Välj ditt konto och ange sedan din unika API-nyckel.</2><3>Testa din Trigger.</3><4>Du är klar!</4>",
|
||||
"make_setup_instructions": "<0>Gå till <1><0>Skapa inbjudningslänk</0></1> och installera Cal.com-appen.</0><1>Logga in på ditt Make-konto och skapa ett nytt Scenario.</1><2>Välj Cal.com som din Trigger-app. Välj också en triggerhändelse.</2><3>Välj ditt konto och ange sedan din unika API-nyckel.</3><4>Testa din trigger.</4><5>Du är redo!</5>",
|
||||
"install_zapier_app": "Installera först Zapier-appen i appbutiken.",
|
||||
"install_make_app": "Installera först Make-appen i appbutiken.",
|
||||
"connect_apple_server": "Anslut till Apple Server",
|
||||
"calendar_url": "Kalender-URL",
|
||||
"apple_server_generate_password": "Skapa ett appspecifikt lösenord som du kan använda med {{appName}} på",
|
||||
@@ -1085,6 +1115,7 @@
|
||||
"email_attendee_action": "skicka e-post till deltagare",
|
||||
"sms_attendee_action": "Skicka SMS till deltagare",
|
||||
"sms_number_action": "skicka SMS till ett specifikt nummer",
|
||||
"send_reminder_sms": "Skicka enkelt mötespåminnelser via SMS till dina deltagare",
|
||||
"whatsapp_number_action": "skicka Whatsapp till ett specifikt nummer",
|
||||
"whatsapp_attendee_action": "skicka Whatsapp till deltagare",
|
||||
"workflows": "Arbetsflöden",
|
||||
@@ -1244,6 +1275,7 @@
|
||||
"error_updating_settings": "Fel vid uppdatering av inställningar",
|
||||
"personal_cal_url": "Min personliga {{appName}}-URL",
|
||||
"bio_hint": "Några meningar om dig själv. Detta kommer att visas på din personliga webbsida.",
|
||||
"user_has_no_bio": "Den här användaren har inte lagt till en biografi ännu.",
|
||||
"delete_account_modal_title": "Radera konto",
|
||||
"confirm_delete_account_modal": "Är du säker på att du vill radera ditt {{appName}}-konto?",
|
||||
"delete_my_account": "Radera mitt konto",
|
||||
@@ -1426,6 +1458,7 @@
|
||||
"add_limit": "Lägg till gräns",
|
||||
"team_name_required": "Teamnamn krävs",
|
||||
"show_attendees": "Dela deltagarinformation mellan gäster",
|
||||
"show_available_seats_count": "Visa antalet lediga platser",
|
||||
"how_booking_questions_as_variables": "Hur används bokningsfrågor som variabler?",
|
||||
"format": "Formatera",
|
||||
"uppercase_for_letters": "Använd versaler för alla bokstäver",
|
||||
@@ -1659,8 +1692,11 @@
|
||||
"delete_sso_configuration_confirmation_description": "Vill du verkligen ta bort {{connectionType}}-konfigurationen? Dina teammedlemmar som använder {{connectionType}}-inloggning kan inte längre komma åt Cal.com.",
|
||||
"organizer_timezone": "Organisatörens tidszon",
|
||||
"email_user_cta": "Visa inbjudan",
|
||||
"email_no_user_invite_heading_team": "Du har blivit inbjuden att gå med i ett {{appName}}-team",
|
||||
"email_no_user_invite_heading_org": "Du har blivit inbjuden att gå med i en {{appName}}-organisation",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i sitt team på {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} har bjudit in dig att gå med i sitt team {{teamName}} den {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} har bjudit in dig att gå med i deras organisation \"{{teamName}}\" på {{appName}}. {{appName}} är schemaläggaren som gör det möjligt för dig och din organisation att schemalägga möten utan e-posttennis.",
|
||||
"email_no_user_invite_steps_intro": "Vi guidar dig genom några korta steg och du kommer att kunna schemalägga med din/ditt {{entity}} på nolltid utan stress.",
|
||||
"email_no_user_step_one": "Välj ditt användarnamn",
|
||||
"email_no_user_step_two": "Anslut ditt kalenderkonto",
|
||||
@@ -1831,6 +1867,8 @@
|
||||
"insights_no_data_found_for_filter": "Inga data hittades för valt filter eller valda datum.",
|
||||
"acknowledge_booking_no_show_fee": "Jag bekräftar att en avgift på {{amount, currency}} för utebliven närvaro debiteras mitt kort om jag inte deltar i händelsen.",
|
||||
"card_details": "Kortuppgifter",
|
||||
"something_went_wrong_on_our_end": "Något gick fel från vår sida. Kontakta vårt supportteam så fixar vi det åt dig direkt.",
|
||||
"please_provide_following_text_to_suppport": "Ange följande text när du kontaktar supporten så att vi kan hjälpa dig bättre",
|
||||
"seats_and_no_show_fee_error": "För närvarande går det inte att aktivera platser och ta ut en avgift för utebliven närvaro",
|
||||
"complete_your_booking": "Slutför din bokning",
|
||||
"complete_your_booking_subject": "Slutför din bokning: {{title}} {{date}}",
|
||||
@@ -1864,6 +1902,9 @@
|
||||
"open_dialog_with_element_click": "Öppna din Cal-dialogruta när någon klickar på ett element.",
|
||||
"need_help_embedding": "Behöver du hjälp? Se våra guider för inbäddning av Cal på Wix, Squarespace eller WordPress, kolla in våra vanliga frågor eller utforska avancerade inbäddningsalternativ.",
|
||||
"book_my_cal": "Boka i min Cal",
|
||||
"first_name": "Förnamn",
|
||||
"last_name": "Efternamn",
|
||||
"first_last_name": "Förnamn, Efternamn",
|
||||
"invite_as": "Bjud in som",
|
||||
"form_updated_successfully": "Formuläret har uppdaterats.",
|
||||
"disable_attendees_confirmation_emails": "Inaktivera förinställda e-postbekräftelser för deltagare",
|
||||
@@ -1877,6 +1918,7 @@
|
||||
"first_event_type_webhook_description": "Skapa din första webhook för denna händelsetyp",
|
||||
"install_app_on": "Installera appen på",
|
||||
"create_for": "Skapa för",
|
||||
"currency": "Valuta",
|
||||
"organization_banner_description": "Skapa miljöer där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
|
||||
"organization_banner_title": "Hantera organisationer med flera team",
|
||||
"set_up_your_organization": "Konfigurera din organisation",
|
||||
@@ -1937,6 +1979,79 @@
|
||||
"insights_team_filter": "Team: {{teamName}}",
|
||||
"insights_user_filter": "Användare: {{userName}}",
|
||||
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
|
||||
"location_options": "{{locationCount}} platsalternativ",
|
||||
"custom_plan": "Anpassad plan",
|
||||
"email_embed": "E-post inbäddad",
|
||||
"add_times_to_your_email": "Välj några tillgängliga tider och bädda in dem i din e-post",
|
||||
"select_time": "Välj tid",
|
||||
"select_date": "Välj datum",
|
||||
"see_all_available_times": "Se alla tillgängliga tider",
|
||||
"org_team_names_example": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_1": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_2": "t.ex. säljteam",
|
||||
"org_team_names_example_3": "t.ex. designteam",
|
||||
"org_team_names_example_4": "t.ex. ingenjörsteam",
|
||||
"org_team_names_example_5": "t.ex. dataanalytikerteam",
|
||||
"org_max_team_warnings": "Du kommer att kunna lägga till fler team senare.",
|
||||
"what_is_this_meeting_about": "Vad handlar det här mötet om?",
|
||||
"add_to_team": "Lägg till i teamet",
|
||||
"remove_users_from_org": "Ta bort användare från organisationen",
|
||||
"remove_users_from_org_confirm": "Är du säker på att du vill ta bort {{userCount}} användare från denna organisation?",
|
||||
"user_has_no_schedules": "Den här användaren har inte skapat några scheman ännu",
|
||||
"user_isnt_in_any_teams": "Den här användaren är inte med i något team",
|
||||
"requires_booker_email_verification": "Kräver e-postverifiering för bokare",
|
||||
"description_requires_booker_email_verification": "För att säkerställa bokarens e-postverifiering före schemaläggning av evenemang",
|
||||
"requires_confirmation_mandatory": "Textmeddelanden kan endast skickas till deltagare när evenemangstypen kräver bekräftelse.",
|
||||
"kyc_verification_information": "För att garantera säkerheten måste du verifiera ditt {{teamOrAccount}} innan du skickar textmeddelanden till deltagare. Kontakta oss på <a>{{supportEmail}}</a> och ange följande information:",
|
||||
"kyc_verification_documents": "<ul><li>Din {{teamOrUser}}</li><li>För företag: Bifoga din dokumentation för verifiering av företag</li><li>För privatpersoner: Bifoga ett statligt utfärdat ID-kort</li></ul>",
|
||||
"verify_team_or_account": "Verifiera {{teamOrAccount}}",
|
||||
"verify_account": "Verifiera konto",
|
||||
"kyc_verification": "KYC-verifiering",
|
||||
"organizations": "Organisationer",
|
||||
"org_admin_other_teams": "Andra team",
|
||||
"org_admin_other_teams_description": "Här kan du se team i din organisation som du inte är en del av. Du kan lägga till dem om det behövs.",
|
||||
"no_other_teams_found": "Inga andra team hittades",
|
||||
"no_other_teams_found_description": "Det finns inga andra team i denna organisation.",
|
||||
"attendee_first_name_variable": "Deltagarens förnamn",
|
||||
"attendee_last_name_variable": "Deltagarens efternamn",
|
||||
"attendee_first_name_info": "Förnamnet på personen som bokat",
|
||||
"attendee_last_name_info": "Efternamnet på personen som bokat",
|
||||
"me": "Jag",
|
||||
"verify_team_tooltip": "Verifiera ditt team för att aktivera att skicka meddelanden till deltagare",
|
||||
"member_removed": "Medlem borttagen",
|
||||
"my_availability": "Min tillgänglighet",
|
||||
"team_availability": "Teamets tillgänglighet",
|
||||
"backup_code": "Säkerhetskopiera kod",
|
||||
"backup_codes": "Säkerhetskopiera koder",
|
||||
"backup_code_instructions": "Varje säkerhetskopierad kod kan användas exakt en gång för att ge åtkomst utan din autentiserare.",
|
||||
"backup_codes_copied": "Säkerhetskopierade koder kopierade!",
|
||||
"incorrect_backup_code": "Den säkerhetskopierade koden är felaktig.",
|
||||
"lost_access": "Förlorad åtkomst",
|
||||
"missing_backup_codes": "Inga säkerhetskopieringskoder hittades. Generera dem i dina inställningar.",
|
||||
"admin_org_notification_email_subject": "Ny organisation skapad: väntande åtgärd",
|
||||
"hi_admin": "Hej administratör",
|
||||
"admin_org_notification_email_title": "En organisation kräver DNS-inställning",
|
||||
"admin_org_notification_email_body_part1": "En organisation med slug \"{{orgSlug}}\" skapades.<br /><br />Se till att konfigurera ditt DNS-register så att underdomänen motsvarar den nya organisationen där huvudappen körs. Annars kommer organisationen inte att fungera.<br /><br />Här är bara de allra grundläggande alternativen för att konfigurera en underdomän för att peka på deras app så den laddar organisationens profilsida.<br /><br />Du kan göra det antingen med A-posten:",
|
||||
"admin_org_notification_email_body_part2": "Eller CNAME-posten:",
|
||||
"admin_org_notification_email_body_part3": "När du har konfigurerat underdomänen, markera DNS-konfigurationen som gjort i organisationens admin-inställningar.",
|
||||
"admin_org_notification_email_cta": "Gå till organisationens admin-inställningar",
|
||||
"org_has_been_processed": "Org har behandlats",
|
||||
"org_error_processing": "Det har uppstått ett fel vid behandlingen av denna organisation",
|
||||
"orgs_page_description": "En lista över alla organisationer. Genom att acceptera en organisation kan alla användare med den e-postdomänen registrera sig UTAN e-postverifiering.",
|
||||
"unverified": "Overifierad",
|
||||
"dns_missing": "DNS saknas",
|
||||
"mark_dns_configured": "Markera som DNS konfigurerad",
|
||||
"value": "Värde",
|
||||
"your_organization_updated_sucessfully": "Din organisation har uppdaterats",
|
||||
"team_no_event_types": "Detta team har inga händelsetyper",
|
||||
"seat_options_doesnt_multiple_durations": "Platsalternativet stöder inte flera varaktigheter",
|
||||
"include_calendar_event": "Inkludera kalenderhändelse",
|
||||
"recently_added": "Nyligen tillagda",
|
||||
"no_members_found": "Inga medlemmar hittades",
|
||||
"event_setup_length_error": "Konfigurering av evenemang: Längden måste vara minst 1 minut.",
|
||||
"availability_schedules": "Scheman för tillgänglighet",
|
||||
"view_only_edit_availability_not_onboarded": "Den här användaren har inte slutfört introduktionen. Du kommer inte att kunna ange deras tillgänglighet förrän de har slutfört introduktionen.",
|
||||
"view_only_edit_availability": "Du tittar på användarens tillgänglighet. Du kan bara redigera din egen tillgänglighet.",
|
||||
"edit_users_availability": "Redigera användarens tillgänglighet: {{username}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -11,18 +11,17 @@ it("correctly handles 1 day", async () => {
|
||||
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1
|
||||
scheduleId: 1,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
locale: 'en',
|
||||
hour12: true
|
||||
locale: "en",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(replaceUnicodeSpace(result)).toBe("Mon, 9:00 AM - 5:00 PM");
|
||||
});
|
||||
|
||||
|
||||
it("correctly handles all days", async () => {
|
||||
const availability = {
|
||||
id: 1,
|
||||
@@ -32,12 +31,12 @@ it("correctly handles all days", async () => {
|
||||
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1
|
||||
scheduleId: 1,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
locale: 'en',
|
||||
hour12: true
|
||||
locale: "en",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(replaceUnicodeSpace(result)).toBe("Mon - Sun, 9:00 AM - 5:00 PM");
|
||||
@@ -52,12 +51,12 @@ it("correctly handles staggered days", async () => {
|
||||
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1
|
||||
scheduleId: 1,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
locale: 'en',
|
||||
hour12: true
|
||||
locale: "en",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(replaceUnicodeSpace(result)).toBe("Mon, Wed, Fri, Sun, 9:00 AM - 5:00 PM");
|
||||
@@ -72,12 +71,12 @@ it("correctly produces days and times - 12 hours", async () => {
|
||||
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1
|
||||
scheduleId: 1,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
locale: 'en',
|
||||
hour12: true
|
||||
locale: "en",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 9:00 AM - 5:00 PM");
|
||||
@@ -92,12 +91,12 @@ it("correctly produces days and times - 24 hours", async () => {
|
||||
startTime: new Date(Date.UTC(1970, 1, 1, 9, 0, 0, 0)),
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1
|
||||
scheduleId: 1,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
locale: 'en',
|
||||
hour12: false
|
||||
locale: "en",
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
expect(replaceUnicodeSpace(result)).toBe("Mon - Wed, 09:00 - 17:00");
|
||||
@@ -107,5 +106,5 @@ it("correctly produces days and times - 24 hours", async () => {
|
||||
// produces Unicode characters. Instead of using line for line code from the
|
||||
// availability.ts file, opted for this instead.
|
||||
const replaceUnicodeSpace = (string: string) => {
|
||||
return string.replace(/\u202f/g, ' ');
|
||||
}
|
||||
return string.replace(/\u202f/g, " ");
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { checkBookingLimits, checkBookingLimit } from "@calcom/lib/server";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { checkDurationLimit, checkDurationLimits } from "@calcom/lib/server";
|
||||
|
||||
|
||||
type MockData = {
|
||||
id: number;
|
||||
startDate: Date;
|
||||
|
||||
@@ -2,10 +2,9 @@ import { expect, it, beforeAll, vi } from "vitest";
|
||||
|
||||
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours";
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
|
||||
})
|
||||
});
|
||||
|
||||
const HAWAII_AND_NEWYORK_TEAM = [
|
||||
{
|
||||
|
||||
@@ -4,10 +4,9 @@ import { expect, it, beforeAll, vi } from "vitest";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
|
||||
})
|
||||
});
|
||||
|
||||
//parse "hh:mm-hh:mm" into <Availability> object
|
||||
const parseWorkingHours = (workingHours: string) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import prisma from "@calcom/prisma";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
|
||||
import { getDate, getGoogleCalendarCredential, createBookingScenario } from "../utils/bookingScenario";
|
||||
|
||||
// TODO: Mock properly
|
||||
@@ -16,7 +17,7 @@ prismaMock.user.findMany.mockResolvedValue([]);
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
WEBAPP_URL: "http://localhost:3000"
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
}));
|
||||
|
||||
declare global {
|
||||
@@ -143,7 +144,6 @@ const TestData = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const cleanup = async () => {
|
||||
await prisma.eventType.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
@@ -296,7 +296,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// getSchedule returns timeslots in GMT
|
||||
@@ -328,7 +328,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
@@ -393,7 +393,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||
@@ -429,7 +429,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
// `slotInterval` takes precedence over `length`
|
||||
// 4:30 is utc so it is 10:00 in IST
|
||||
@@ -493,7 +493,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -514,7 +514,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -578,7 +578,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||
@@ -652,7 +652,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||
@@ -710,7 +710,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
@@ -774,7 +774,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||
@@ -852,7 +852,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(thisUserAvailability).toHaveTimeSlots(
|
||||
@@ -950,7 +950,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
@@ -980,7 +980,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
@@ -1088,7 +1088,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||
@@ -1116,7 +1116,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getWorkingHours } from "@calcom/lib/availability";
|
||||
|
||||
beforeAll(() => {
|
||||
vi.setSystemTime(new Date("2021-06-20T11:59:59Z"));
|
||||
})
|
||||
});
|
||||
|
||||
it("correctly translates Availability (UTC+0) to UTC workingHours", async () => {
|
||||
expect(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { EventType } from "@prisma/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -6,8 +8,6 @@ import { buildEventType } from "@calcom/lib/test/builder";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
|
||||
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
const mockFindFirstEventType = (data?: Partial<CompleteEventType>) => {
|
||||
const eventType = buildEventType(data as Partial<EventType>);
|
||||
prismaMock.eventType.findFirst.mockResolvedValue(eventType as EventType);
|
||||
@@ -97,11 +97,12 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [], team: { name: "" } },
|
||||
@@ -133,11 +134,21 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, locations, parentId, userId, scheduleId,requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
teamId,
|
||||
timeZone,
|
||||
locations,
|
||||
parentId,
|
||||
userId,
|
||||
scheduleId,
|
||||
requiresBookerEmailVerification,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
|
||||
@@ -218,11 +229,12 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users,requiresBookerEmailVerification, ...evType } = mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
id: 123,
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
@@ -255,11 +267,20 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const { schedulingType, id, teamId, timeZone, users, locations, parentId, userId,requiresBookerEmailVerification, ...evType } =
|
||||
mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
const {
|
||||
schedulingType,
|
||||
id,
|
||||
teamId,
|
||||
timeZone,
|
||||
locations,
|
||||
parentId,
|
||||
userId,
|
||||
requiresBookerEmailVerification,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
locations: [],
|
||||
});
|
||||
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { it, expect, describe, beforeAll } from "vitest";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -30,67 +31,50 @@ beforeAll(async () => {
|
||||
|
||||
describe("next.config.js - Org Rewrite", () => {
|
||||
const orgHostRegExp = (subdomainRegExp: string) =>
|
||||
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
|
||||
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
|
||||
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`);
|
||||
|
||||
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
|
||||
it("https://app.cal.com", () => {
|
||||
const subdomainRegExp = getSubdomainRegExp("https://app.cal.com");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups
|
||||
?.orgSlug
|
||||
).toEqual("company");
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("org.cal.com")?.groups?.orgSlug
|
||||
).toEqual("org");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("org.cal.com")?.groups?.orgSlug).toEqual("org");
|
||||
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("localhost:3000")
|
||||
).toEqual(null);
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("localhost:3000")).toEqual(null);
|
||||
});
|
||||
|
||||
it("app.cal.com", () => {
|
||||
const subdomainRegExp = getSubdomainRegExp("app.cal.com");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("app.cal.com")).toEqual(null);
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups
|
||||
?.orgSlug
|
||||
).toEqual("company");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("company.app.cal.com")?.groups?.orgSlug).toEqual("company");
|
||||
});
|
||||
|
||||
it("https://calcom.app.company.com", () => {
|
||||
const subdomainRegExp = getSubdomainRegExp(
|
||||
"https://calcom.app.company.com"
|
||||
const subdomainRegExp = getSubdomainRegExp("https://calcom.app.company.com");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("calcom.app.company.com")).toEqual(null);
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.app.company.com")?.groups?.orgSlug).toEqual(
|
||||
"acme"
|
||||
);
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("calcom.app.company.com")
|
||||
).toEqual(null);
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("acme.calcom.app.company.com")
|
||||
?.groups?.orgSlug
|
||||
).toEqual("acme");
|
||||
});
|
||||
|
||||
it("https://calcom.example.com", () => {
|
||||
const subdomainRegExp = getSubdomainRegExp("https://calcom.example.com");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("calcom.example.com")).toEqual(
|
||||
null
|
||||
);
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("acme.calcom.example.com")?.groups
|
||||
?.orgSlug
|
||||
).toEqual("acme");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("calcom.example.com")).toEqual(null);
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("acme.calcom.example.com")?.groups?.orgSlug).toEqual("acme");
|
||||
// The following also matches which causes anything other than the domain in NEXT_PUBLIC_WEBAPP_URL to give 404
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("some-other.company.com")?.groups
|
||||
?.orgSlug
|
||||
).toEqual("some-other");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("some-other.company.com")?.groups?.orgSlug).toEqual(
|
||||
"some-other"
|
||||
);
|
||||
});
|
||||
it("Should ignore Vercel preview URLs", () => {
|
||||
const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
|
||||
expect(
|
||||
orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")
|
||||
).toMatchInlineSnapshot("null");
|
||||
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot(
|
||||
"null"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { getLuckyUser } from "@calcom/lib/server";
|
||||
import { buildUser } from "@calcom/lib/test/builder";
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
|
||||
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type {
|
||||
EventType as PrismaEventType,
|
||||
User as PrismaUser,
|
||||
@@ -17,10 +21,6 @@ import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
|
||||
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
type App = {
|
||||
slug: string;
|
||||
dirName: string;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
items:
|
||||
- iframe: { src: https://www.youtube.com/embed/tExtOJqnI0Q }
|
||||
- 1.jpg
|
||||
- 2.png
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Alby",
|
||||
"slug": "alby",
|
||||
"type": "alby_payment",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://example.com/link",
|
||||
"variant": "payment",
|
||||
"categories": ["payment"],
|
||||
"publisher": "Alby",
|
||||
"email": "support@getalby.com",
|
||||
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "alby"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as api from "./api";
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/alby",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="554" height="554" viewBox="0 0 554 554" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="554" height="554" rx="64" fill="white"/>
|
||||
<path d="M127.7 202.4C107.431 202.4 90.9999 185.969 90.9999 165.7C90.9999 145.431 107.431 129 127.7 129C147.969 129 164.4 145.431 164.4 165.7C164.4 185.969 147.969 202.4 127.7 202.4Z" fill="black"/>
|
||||
<path d="M121.6 160.2L190.1 228.7" stroke="black" stroke-width="18.3"/>
|
||||
<path d="M427.2 202.4C447.469 202.4 463.9 185.969 463.9 165.7C463.9 145.431 447.469 129 427.2 129C406.931 129 390.5 145.431 390.5 165.7C390.5 185.969 406.931 202.4 427.2 202.4Z" fill="black"/>
|
||||
<path d="M434 160.2L365.5 228.7" stroke="black" stroke-width="18.3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M138.1 384.6C128.451 380.122 120.543 372.597 115.592 363.182C110.641 353.768 108.922 342.987 110.7 332.5C127.3 240 196.2 170.6 278.7 170.6C361.4 170.6 430.5 240.3 446.8 333.2C448.539 343.695 446.779 354.47 441.792 363.867C436.805 373.263 428.866 380.759 419.2 385.2C375.441 405.799 327.664 416.454 279.3 416.4C228.8 416.4 180.9 405 138.1 384.6Z" fill="#FFDF6F"/>
|
||||
<path d="M119.8 334.2C135.8 244.7 201.8 179.8 278.8 179.8V161.5C190.6 161.5 118.8 235.3 101.7 330.9L119.7 334.1L119.8 334.2ZM278.8 179.8C355.8 179.8 422 245.1 437.8 334.8L455.8 331.6C439 235.6 367 161.5 278.8 161.5V179.8ZM415.3 376.9C372.76 396.917 326.314 407.265 279.3 407.2V425.5C330.7 425.5 379.4 414 423.1 393.5L415.3 376.9ZM279.3 407.2C230.2 407.2 183.7 396.1 142.1 376.3L134.2 392.9C178.2 413.9 227.4 425.5 279.3 425.5V407.2ZM437.8 334.8C440.8 352.1 431.6 369.3 415.3 376.9L423.1 393.5C446.5 382.5 460.4 357.5 455.8 331.5L437.8 334.8ZM101.7 330.8C99.5558 343.269 101.577 356.098 107.451 367.303C113.325 378.509 122.725 387.47 134.2 392.8L142.1 376.3C134.267 372.688 127.84 366.599 123.81 358.973C119.78 351.347 118.371 342.606 119.8 334.1L101.6 330.9L101.7 330.8Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M173.4 361.6C157.5 355.1 148.1 338.2 153.6 321.9C170.6 271.7 220.2 235.4 278.7 235.4C337.2 235.4 386.8 271.7 403.8 321.9C409.4 338.2 399.9 355.1 384 361.6C350.559 375.19 314.796 382.153 278.7 382.1C241.5 382.1 205.9 374.8 173.4 361.6Z" fill="black"/>
|
||||
<path d="M320.9 338.1C337.8 338.1 351.5 327.176 351.5 313.7C351.5 300.224 337.8 289.3 320.9 289.3C304 289.3 290.3 300.224 290.3 313.7C290.3 327.176 304 338.1 320.9 338.1Z" fill="white"/>
|
||||
<path d="M233.4 338.1C250.3 338.1 264 327.176 264 313.7C264 300.224 250.3 289.3 233.4 289.3C216.5 289.3 202.8 300.224 202.8 313.7C202.8 327.176 216.5 338.1 233.4 338.1Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -16,6 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: req.session?.user?.id,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
@@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const dav = new CalendarService({
|
||||
id: 0,
|
||||
...data,
|
||||
user: { email: user.email },
|
||||
});
|
||||
await dav?.listCalendars();
|
||||
await prisma.credential.create({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
This file is autogenerated using the command `yarn app-store:build --watch`.
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
import alby_config_json from "./alby/config.json";
|
||||
import amie_config_json from "./amie/config.json";
|
||||
import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata";
|
||||
import around_config_json from "./around/config.json";
|
||||
@@ -73,6 +74,7 @@ import zohocrm_config_json from "./zohocrm/config.json";
|
||||
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
|
||||
|
||||
export const appStoreMetadata = {
|
||||
alby: alby_config_json,
|
||||
amie: amie_config_json,
|
||||
applecalendar: applecalendar__metadata_ts,
|
||||
around: around_config_json,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
export const apiHandlers = {
|
||||
alby: import("./alby/api"),
|
||||
amie: import("./amie/api"),
|
||||
applecalendar: import("./applecalendar/api"),
|
||||
around: import("./around/api"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
@@ -24,6 +25,7 @@ export const projectMutationHandler = async ({ ctx, input }: ProjectMutationHand
|
||||
where: {
|
||||
userId: user?.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!credential) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
@@ -20,6 +21,7 @@ export const projectHandler = async ({ ctx }: ProjectsHandlerOptions) => {
|
||||
where: {
|
||||
userId: user?.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (!credential) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "No credential found for user" });
|
||||
|
||||
@@ -17,6 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const dav = new CalendarService({
|
||||
id: 0,
|
||||
...data,
|
||||
user: { email: user.email },
|
||||
});
|
||||
await dav?.listCalendars();
|
||||
await prisma.credential.create({
|
||||
|
||||
@@ -60,6 +60,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = {
|
||||
type: "daily_video",
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
userId: 0,
|
||||
user: { email: "" },
|
||||
appId: "daily-video",
|
||||
invalid: false,
|
||||
teamId: null,
|
||||
|
||||
@@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,6 +43,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const dav = new CalendarService({
|
||||
id: 0,
|
||||
...data,
|
||||
user: { email: user.email },
|
||||
});
|
||||
await dav?.listCalendars();
|
||||
await prisma.credential.create({
|
||||
|
||||
@@ -26,6 +26,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,6 +42,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const dav = new CalendarService({
|
||||
id: 0,
|
||||
user: { email: user.email },
|
||||
...data,
|
||||
});
|
||||
await dav?.listCalendars();
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
};
|
||||
|
||||
try {
|
||||
const service = new CalendarService({ id: 0, ...data });
|
||||
const service = new CalendarService({ id: 0, user: { email: session.user.email || "" }, ...data });
|
||||
await service?.listCalendars();
|
||||
await prisma.credential.create({ data });
|
||||
} catch (reason) {
|
||||
|
||||
@@ -28,11 +28,13 @@ export default class GoogleCalendarService implements Calendar {
|
||||
private integrationName = "";
|
||||
private auth: { getToken: () => Promise<MyGoogleAuth> };
|
||||
private log: typeof logger;
|
||||
private credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.integrationName = "google_calendar";
|
||||
this.auth = this.googleAuth(credential);
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
private googleAuth = (credential: CredentialPayload) => {
|
||||
@@ -84,22 +86,50 @@ export default class GoogleCalendarService implements Calendar {
|
||||
};
|
||||
};
|
||||
|
||||
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
|
||||
private getAttendees = (event: CalendarEvent) => {
|
||||
// When rescheduling events we know the external id of the calendar so we can just look for it in the destinationCalendar array.
|
||||
const selectedHostDestinationCalendar = event.destinationCalendar?.find(
|
||||
(cal) => cal.credentialId === this.credential.id
|
||||
);
|
||||
const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({
|
||||
...rest,
|
||||
responseStatus: "accepted",
|
||||
}));
|
||||
// TODO: Check every other CalendarService for team members
|
||||
const teamMembers =
|
||||
calEventRaw.team?.members.map((m) => ({
|
||||
email: m.email,
|
||||
displayName: m.name,
|
||||
const attendees: calendar_v3.Schema$EventAttendee[] = [
|
||||
{
|
||||
...event.organizer,
|
||||
id: String(event.organizer.id),
|
||||
responseStatus: "accepted",
|
||||
})) || [];
|
||||
organizer: true,
|
||||
// Tried changing the display name to the user but GCal will not let you do that. It will only display the name of the external calendar. Leaving this in just incase it works in the future.
|
||||
displayName: event.organizer.name,
|
||||
email: selectedHostDestinationCalendar?.externalId ?? event.organizer.email,
|
||||
},
|
||||
...eventAttendees,
|
||||
];
|
||||
|
||||
if (event.team?.members) {
|
||||
// TODO: Check every other CalendarService for team members
|
||||
const teamAttendeesWithoutCurrentUser = event.team.members
|
||||
.filter((member) => member.email !== this.credential.user?.email)
|
||||
.map((m) => {
|
||||
const teamMemberDestinationCalendar = event.destinationCalendar?.find(
|
||||
(calendar) => calendar.integration === "google_calendar" && calendar.userId === m.id
|
||||
);
|
||||
return {
|
||||
email: teamMemberDestinationCalendar?.externalId ?? m.email,
|
||||
displayName: m.name,
|
||||
responseStatus: "accepted",
|
||||
};
|
||||
});
|
||||
attendees.push(...teamAttendeesWithoutCurrentUser);
|
||||
}
|
||||
|
||||
return attendees;
|
||||
};
|
||||
|
||||
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const selectedHostDestinationCalendar = calEventRaw.destinationCalendar?.find(
|
||||
(cal) => cal.credentialId === credentialId
|
||||
);
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: calEventRaw.title,
|
||||
@@ -112,19 +142,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
dateTime: calEventRaw.endTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
...calEventRaw.organizer,
|
||||
id: String(calEventRaw.organizer.id),
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
email: selectedHostDestinationCalendar?.externalId
|
||||
? selectedHostDestinationCalendar.externalId
|
||||
: calEventRaw.organizer.email,
|
||||
},
|
||||
...eventAttendees,
|
||||
...teamMembers,
|
||||
],
|
||||
attendees: this.getAttendees(calEventRaw),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
@@ -194,19 +212,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const [mainHostDestinationCalendar] =
|
||||
event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : [];
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const eventAttendees = event.attendees.map(({ ...rest }) => ({
|
||||
...rest,
|
||||
responseStatus: "accepted",
|
||||
}));
|
||||
const teamMembers =
|
||||
event.team?.members.map((m) => ({
|
||||
email: m.email,
|
||||
displayName: m.name,
|
||||
responseStatus: "accepted",
|
||||
})) || [];
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
@@ -218,19 +224,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
...event.organizer,
|
||||
id: String(event.organizer.id),
|
||||
organizer: true,
|
||||
responseStatus: "accepted",
|
||||
email: mainHostDestinationCalendar?.externalId
|
||||
? mainHostDestinationCalendar.externalId
|
||||
: event.organizer.email,
|
||||
},
|
||||
...(eventAttendees as any),
|
||||
...(teamMembers as any),
|
||||
],
|
||||
attendees: this.getAttendees(event),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
|
||||
@@ -34,11 +34,13 @@ export default class LarkCalendarService implements Calendar {
|
||||
private integrationName = "";
|
||||
private log: typeof logger;
|
||||
auth: { getToken: () => Promise<string> };
|
||||
private credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.integrationName = "lark_calendar";
|
||||
this.auth = this.larkAuth(credential);
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
private larkAuth = (credential: CredentialPayload) => {
|
||||
@@ -122,10 +124,13 @@ export default class LarkCalendarService implements Calendar {
|
||||
});
|
||||
};
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
let eventId = "";
|
||||
let eventRespData;
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const mainHostDestinationCalendar = event.destinationCalendar
|
||||
? event.destinationCalendar.find((cal) => cal.credentialId === this.credential.id) ??
|
||||
event.destinationCalendar[0]
|
||||
: undefined;
|
||||
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
throw new Error("no calendar id");
|
||||
@@ -143,7 +148,7 @@ export default class LarkCalendarService implements Calendar {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.createAttendees(event, eventId);
|
||||
await this.createAttendees(event, eventId, credentialId);
|
||||
return {
|
||||
...eventRespData,
|
||||
uid: eventRespData.data.event.event_id as string,
|
||||
@@ -160,8 +165,11 @@ export default class LarkCalendarService implements Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
private createAttendees = async (event: CalendarEvent, eventId: string) => {
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
private createAttendees = async (event: CalendarEvent, eventId: string, credentialId: number) => {
|
||||
const mainHostDestinationCalendar = event.destinationCalendar
|
||||
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
|
||||
event.destinationCalendar[0]
|
||||
: undefined;
|
||||
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
this.log.error("no calendar id provided in createAttendees");
|
||||
@@ -189,7 +197,9 @@ export default class LarkCalendarService implements Calendar {
|
||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
||||
const eventId = uid;
|
||||
let eventRespData;
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const mainHostDestinationCalendar = event.destinationCalendar?.find(
|
||||
(cal) => cal.externalId === externalCalendarId
|
||||
);
|
||||
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
this.log.error("no calendar id provided in updateEvent");
|
||||
@@ -234,7 +244,9 @@ export default class LarkCalendarService implements Calendar {
|
||||
* @returns
|
||||
*/
|
||||
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const mainHostDestinationCalendar = event.destinationCalendar?.find(
|
||||
(cal) => cal.externalId === externalCalendarId
|
||||
);
|
||||
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
this.log.error("no calendar id provided in deleteEvent");
|
||||
@@ -393,13 +405,16 @@ export default class LarkCalendarService implements Calendar {
|
||||
attendeeArray.push(attendee);
|
||||
});
|
||||
event.team?.members.forEach((member) => {
|
||||
const attendee: LarkEventAttendee = {
|
||||
type: "third_party",
|
||||
is_optional: false,
|
||||
third_party_email: member.email,
|
||||
};
|
||||
attendeeArray.push(attendee);
|
||||
if (member.email !== this.credential.user?.email) {
|
||||
const attendee: LarkEventAttendee = {
|
||||
type: "third_party",
|
||||
is_optional: false,
|
||||
third_party_email: member.email,
|
||||
};
|
||||
attendeeArray.push(attendee);
|
||||
}
|
||||
});
|
||||
|
||||
return attendeeArray;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,16 +61,20 @@ export default class Office365CalendarService implements Calendar {
|
||||
private accessToken: string | null = null;
|
||||
auth: { getToken: () => Promise<string> };
|
||||
private apiGraphUrl = "https://graph.microsoft.com/v1.0";
|
||||
private credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.integrationName = "office365_calendar";
|
||||
this.auth = this.o365Auth(credential);
|
||||
|
||||
this.credential = credential;
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
const mainHostDestinationCalendar = event.destinationCalendar
|
||||
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
|
||||
event.destinationCalendar[0]
|
||||
: undefined;
|
||||
try {
|
||||
const eventsUrl = mainHostDestinationCalendar?.externalId
|
||||
? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events`
|
||||
@@ -295,6 +299,16 @@ export default class Office365CalendarService implements Calendar {
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: [
|
||||
// Add the calEvent organizer
|
||||
{
|
||||
emailAddress: {
|
||||
address: event.destinationCalendar
|
||||
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
|
||||
event.organizer.email
|
||||
: event.organizer.email,
|
||||
name: event.organizer.name,
|
||||
},
|
||||
},
|
||||
...event.attendees.map((attendee) => ({
|
||||
emailAddress: {
|
||||
address: attendee.email,
|
||||
@@ -303,13 +317,20 @@ export default class Office365CalendarService implements Calendar {
|
||||
type: "required",
|
||||
})),
|
||||
...(event.team?.members
|
||||
? event.team?.members.map((member) => ({
|
||||
emailAddress: {
|
||||
address: member.email,
|
||||
name: member.name,
|
||||
},
|
||||
type: "required",
|
||||
}))
|
||||
? event.team?.members
|
||||
.filter((member) => member.email !== this.credential.user?.email)
|
||||
.map((member) => {
|
||||
const destinationCalendar =
|
||||
event.destinationCalendar &&
|
||||
event.destinationCalendar.find((cal) => cal.userId === member.id);
|
||||
return {
|
||||
emailAddress: {
|
||||
address: destinationCalendar?.externalId ?? member.email,
|
||||
name: member.name,
|
||||
},
|
||||
type: "required",
|
||||
};
|
||||
})
|
||||
: []),
|
||||
],
|
||||
location: event.location ? { displayName: getLocation(event) } : undefined,
|
||||
@@ -454,12 +475,13 @@ export default class Office365CalendarService implements Calendar {
|
||||
(acc: BufferedBusyTime[], subResponse: { body: { value?: BodyValue[]; error?: Error[] } }) => {
|
||||
if (!subResponse.body?.value) return acc;
|
||||
return acc.concat(
|
||||
subResponse.body.value
|
||||
.filter((evt) => evt.showAs !== "free" && evt.showAs !== "workingElsewhere")
|
||||
.map((evt) => ({
|
||||
subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => {
|
||||
if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc;
|
||||
return acc.concat({
|
||||
start: evt.start.dateTime + "Z",
|
||||
end: evt.end.dateTime + "Z",
|
||||
}))
|
||||
});
|
||||
}, [])
|
||||
);
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -53,7 +53,7 @@ class Paypal {
|
||||
const { access_token, expires_in } = await response.json();
|
||||
this.accessToken = access_token;
|
||||
this.expiresAt = Date.now() + expires_in;
|
||||
} else {
|
||||
} else if (response?.status) {
|
||||
console.error(`Request failed with status ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -127,7 +127,7 @@ class Paypal {
|
||||
});
|
||||
if (captureResult.ok) {
|
||||
const result = await captureResult.json();
|
||||
if (result.body.status === "COMPLETED") {
|
||||
if (result?.status === "COMPLETED") {
|
||||
// Get payment reference id
|
||||
|
||||
const payment = await prisma.payment.findFirst({
|
||||
@@ -153,7 +153,7 @@ class Paypal {
|
||||
success: true,
|
||||
data: Object.assign(
|
||||
{},
|
||||
{ ...(payment?.data as Record<string, string | number>), capture: result.body.id }
|
||||
{ ...(payment?.data as Record<string, string | number>), capture: result.id }
|
||||
) as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Head from "next/head";
|
||||
import { stringify } from "querystring";
|
||||
import z from "zod";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { TRPCError } from "@calcom/trpc";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
|
||||
@@ -92,11 +94,22 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
const { default: trpcRouter } = await import("@calcom/app-store/routing-forms/trpc/_router");
|
||||
const caller = trpcRouter.createCaller(ctx);
|
||||
const { v4: uuidv4 } = await import("uuid");
|
||||
await caller.public.response({
|
||||
formId: form.id,
|
||||
formFillerId: uuidv4(),
|
||||
response: response,
|
||||
});
|
||||
try {
|
||||
await caller.public.response({
|
||||
formId: form.id,
|
||||
formFillerId: uuidv4(),
|
||||
response: response,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof TRPCError) {
|
||||
return {
|
||||
props: {
|
||||
form: serializableForm,
|
||||
message: e.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Maybe take action after successful mutation
|
||||
if (decidedAction.type === "customPageMessage") {
|
||||
@@ -109,14 +122,14 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/${decidedAction.value}`,
|
||||
destination: `/${decidedAction.value}?${stringify(context.query)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
} else if (decidedAction.type === "externalRedirectUrl") {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `${decidedAction.value}`,
|
||||
destination: `${decidedAction.value}?${stringify(context.query)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -304,12 +304,14 @@ test.describe("Routing Forms", () => {
|
||||
await users.logout();
|
||||
page.goto(`/router?form=${routingForm.id}&Test field=event-routing`);
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.endsWith("/pro/30min");
|
||||
return url.pathname.endsWith("/pro/30min") && url.searchParams.get("Test field") === "event-routing";
|
||||
});
|
||||
|
||||
page.goto(`/router?form=${routingForm.id}&Test field=external-redirect`);
|
||||
await page.waitForURL((url) => {
|
||||
return url.hostname.includes("google.com");
|
||||
return (
|
||||
url.hostname.includes("google.com") && url.searchParams.get("Test field") === "external-redirect"
|
||||
);
|
||||
});
|
||||
|
||||
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
|
||||
|
||||
@@ -73,12 +73,14 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) =>
|
||||
}
|
||||
return !schema.safeParse(fieldValue).success;
|
||||
})
|
||||
.map((f) => ({ label: f.label, type: f.type }));
|
||||
.map((f) => ({ label: f.label, type: f.type, value: response[f.id]?.value }));
|
||||
|
||||
if (invalidFields.length) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Invalid fields ${invalidFields.map((f) => `${f.label}: ${f.type}`)}`,
|
||||
message: `Invalid value for fields ${invalidFields
|
||||
.map((f) => `'${f.label}' with value '${f.value}' should be valid ${f.type}`)
|
||||
.join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defaultVideoAppCategories } from "@calcom/app-store/utils";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import { defaultLocations } from "./locations";
|
||||
|
||||
@@ -62,13 +63,7 @@ export async function getLocationGroupedOptions(
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
...credentialForCalendarServiceSelect,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
|
||||
@@ -49,9 +49,13 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
async create(
|
||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"],
|
||||
userId: Booking["userId"],
|
||||
username: string | null,
|
||||
bookerName: string,
|
||||
bookerEmail: string,
|
||||
paymentOption: PaymentOption,
|
||||
eventTitle?: string
|
||||
eventTitle?: string,
|
||||
bookingTitle?: string
|
||||
) {
|
||||
try {
|
||||
// Ensure that the payment service can support the passed payment option
|
||||
@@ -82,8 +86,12 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
metadata: {
|
||||
identifier: "cal.com",
|
||||
bookingId,
|
||||
calAccountId: userId,
|
||||
calUsername: username,
|
||||
bookerName,
|
||||
bookerEmail,
|
||||
eventName: eventTitle || "",
|
||||
eventTitle: eventTitle || "",
|
||||
bookingTitle: bookingTitle || "",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { AppCategories } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// If you import this file on any app it should produce circular dependency
|
||||
// import appStore from "./index";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import type { App, AppMeta } from "@calcom/types/App";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
export * from "./_utils/getEventTypeAppData";
|
||||
|
||||
@@ -30,13 +30,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
|
||||
return store;
|
||||
}, {} as Record<string, AppMeta>);
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true, key: true, userId: true, teamId: true, appId: true, invalid: true },
|
||||
});
|
||||
|
||||
export type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
|
||||
export type CredentialDataWithTeamName = CredentialData & {
|
||||
export type CredentialDataWithTeamName = CredentialPayload & {
|
||||
team?: {
|
||||
name: string;
|
||||
} | null;
|
||||
@@ -64,6 +58,7 @@ function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
key: appMeta.key!,
|
||||
userId: 0,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
appId: appMeta.slug,
|
||||
invalid: false,
|
||||
|
||||
@@ -8,6 +8,9 @@ module.exports = {
|
||||
printWidth: 110,
|
||||
arrowParens: "always",
|
||||
importOrder: [
|
||||
// Mocks must be at the top as they contain vi.mock calls
|
||||
"(.*)/__mocks__/(.*)",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@(calcom|ee)/(.*)$",
|
||||
"^@lib/(.*)$",
|
||||
"^@components/(.*)$",
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
import getCalendarsEvents from "./getCalendarsEvents";
|
||||
@@ -216,7 +216,7 @@ export const getBusyCalendarTimes = async (
|
||||
};
|
||||
|
||||
export const createEvent = async (
|
||||
credential: CredentialWithAppName,
|
||||
credential: CredentialPayload,
|
||||
calEvent: CalendarEvent,
|
||||
externalId?: string
|
||||
): Promise<EventResult<NewCalendarEventType>> => {
|
||||
@@ -255,7 +255,7 @@ export const createEvent = async (
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
appName: credential.appId || "",
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
@@ -270,7 +270,7 @@ export const createEvent = async (
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
credential: CredentialWithAppName,
|
||||
credential: CredentialPayload,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null,
|
||||
externalCalendarId: string | null
|
||||
@@ -311,7 +311,7 @@ export const updateEvent = async (
|
||||
}
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
appName: credential.appId || "",
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DestinationCalendar, Booking } from "@prisma/client";
|
||||
import type { Booking, DestinationCalendar } from "@prisma/client";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep, merge } from "lodash";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
@@ -7,18 +7,21 @@ import type { z } from "zod";
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvideo/zod";
|
||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||
import { MeetLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationTypeFromApp, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { Event } from "@calcom/types/Event";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
|
||||
import type {
|
||||
CreateUpdateResult,
|
||||
EventResult,
|
||||
PartialBooking,
|
||||
PartialReference,
|
||||
} from "@calcom/types/EventManager";
|
||||
|
||||
import { createEvent, updateEvent } from "./CalendarManager";
|
||||
import { createMeeting, updateMeeting } from "./videoClient";
|
||||
@@ -68,8 +71,8 @@ export type EventManagerUser = {
|
||||
type createdEventSchema = z.infer<typeof createdEventSchema>;
|
||||
|
||||
export default class EventManager {
|
||||
calendarCredentials: CredentialWithAppName[];
|
||||
videoCredentials: CredentialWithAppName[];
|
||||
calendarCredentials: CredentialPayload[];
|
||||
videoCredentials: CredentialPayload[];
|
||||
|
||||
/**
|
||||
* Takes an array of credentials and initializes a new instance of the EventManager.
|
||||
@@ -338,26 +341,37 @@ export default class EventManager {
|
||||
private async createAllCalendarEvents(event: CalendarEvent) {
|
||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||
for (const destination of event.destinationCalendar) {
|
||||
// Since GCal pushes events to multiple calendars we only want to create one event per booking
|
||||
let gCalAdded = false;
|
||||
const destinationCalendars: DestinationCalendar[] = event.destinationCalendar.reduce(
|
||||
(destinationCals, cal) => {
|
||||
if (cal.integration === "google_calendar") {
|
||||
if (gCalAdded) {
|
||||
return destinationCals;
|
||||
} else {
|
||||
gCalAdded = true;
|
||||
destinationCals.push(cal);
|
||||
}
|
||||
} else {
|
||||
destinationCals.push(cal);
|
||||
}
|
||||
return destinationCals;
|
||||
},
|
||||
[] as DestinationCalendar[]
|
||||
);
|
||||
for (const destination of destinationCalendars) {
|
||||
if (destination.credentialId) {
|
||||
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
|
||||
if (!credential) {
|
||||
// Fetch credential from DB
|
||||
const credentialFromDB = await prisma.credential.findUnique({
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: destination.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||
if (credentialFromDB && credentialFromDB.appId) {
|
||||
credential = {
|
||||
appName: credentialFromDB?.app.slug ?? "",
|
||||
id: credentialFromDB.id,
|
||||
type: credentialFromDB.type,
|
||||
key: credentialFromDB.key,
|
||||
@@ -365,6 +379,7 @@ export default class EventManager {
|
||||
teamId: credentialFromDB.teamId,
|
||||
invalid: credentialFromDB.invalid,
|
||||
appId: credentialFromDB.appId,
|
||||
user: credentialFromDB.user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -416,7 +431,7 @@ export default class EventManager {
|
||||
* @private
|
||||
*/
|
||||
|
||||
private getVideoCredential(event: CalendarEvent): CredentialWithAppName | undefined {
|
||||
private getVideoCredential(event: CalendarEvent): CredentialPayload | undefined {
|
||||
if (!event.location) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -444,7 +459,7 @@ export default class EventManager {
|
||||
event.location +
|
||||
" because credential is missing for the app"
|
||||
);
|
||||
videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
|
||||
videoCredential = { ...FAKE_DAILY_CREDENTIAL };
|
||||
}
|
||||
|
||||
return videoCredential;
|
||||
@@ -524,20 +539,13 @@ export default class EventManager {
|
||||
if (!credential) {
|
||||
// Fetch credential from DB
|
||||
const credentialFromDB = await prisma.credential.findUnique({
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||
if (credentialFromDB && credentialFromDB.appId) {
|
||||
credential = {
|
||||
appName: credentialFromDB?.app.slug ?? "",
|
||||
id: credentialFromDB.id,
|
||||
type: credentialFromDB.type,
|
||||
key: credentialFromDB.key,
|
||||
@@ -545,6 +553,7 @@ export default class EventManager {
|
||||
teamId: credentialFromDB.teamId,
|
||||
invalid: credentialFromDB.invalid,
|
||||
appId: credentialFromDB.appId,
|
||||
user: credentialFromDB.user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -567,6 +576,7 @@ export default class EventManager {
|
||||
where: {
|
||||
id: oldCalendarEvent.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
const calendar = await getCalendar(calendarCredential);
|
||||
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
|
||||
@@ -582,7 +592,7 @@ export default class EventManager {
|
||||
|
||||
if (!calendarReference) {
|
||||
return {
|
||||
appName: cred.appName,
|
||||
appName: cred.appId || "",
|
||||
type: cred.type,
|
||||
success: false,
|
||||
uid: "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Booking, Credential, EventType } from "@prisma/client";
|
||||
import type { Booking, EventType } from "@prisma/client";
|
||||
|
||||
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -9,9 +9,10 @@ import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
export async function getBusyTimes(params: {
|
||||
credentials: Credential[];
|
||||
credentials: CredentialPayload[];
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
username: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
import type {
|
||||
EventBusyDate,
|
||||
@@ -81,7 +82,9 @@ const getUser = (where: Prisma.UserWhereInput) =>
|
||||
where,
|
||||
select: {
|
||||
...availabilityUserSelect,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { GetRecordingsResponseSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoApiAdapterFactory, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
@@ -48,7 +48,7 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
|
||||
results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[])
|
||||
);
|
||||
|
||||
const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => {
|
||||
const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => {
|
||||
const uid: string = getUid(calEvent);
|
||||
|
||||
if (!credential || !credential.appId) {
|
||||
@@ -69,7 +69,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
|
||||
createdEvent: VideoCallData | undefined;
|
||||
credentialId: number;
|
||||
} = {
|
||||
appName: credential.appName,
|
||||
appName: credential.appId || "",
|
||||
type: credential.type,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
@@ -110,7 +110,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
|
||||
};
|
||||
|
||||
const updateMeeting = async (
|
||||
credential: CredentialWithAppName,
|
||||
credential: CredentialPayload,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRef: PartialReference | null
|
||||
): Promise<EventResult<VideoCallData>> => {
|
||||
@@ -131,7 +131,7 @@ const updateMeeting = async (
|
||||
|
||||
if (!updatedMeeting) {
|
||||
return {
|
||||
appName: credential.appName,
|
||||
appName: credential.appId || "",
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
@@ -140,7 +140,7 @@ const updateMeeting = async (
|
||||
}
|
||||
|
||||
return {
|
||||
appName: credential.appName,
|
||||
appName: credential.appId || "",
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
@@ -176,6 +176,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
key: dailyAppKeys,
|
||||
invalid: false,
|
||||
@@ -200,6 +201,7 @@ const getRecordingsOfCalVideoByRoomName = async (
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
key: dailyAppKeys,
|
||||
invalid: false,
|
||||
@@ -222,6 +224,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => {
|
||||
appId: "daily-video",
|
||||
type: "daily_video",
|
||||
userId: null,
|
||||
user: { email: "" },
|
||||
teamId: null,
|
||||
key: dailyAppKeys,
|
||||
invalid: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EmbedThemeConfig } from "./embed-iframe";
|
||||
import type { EmbedThemeConfig } from "./src/types";
|
||||
|
||||
export default function EmbedInitIframe() {
|
||||
if (typeof window === "undefined" || window.isEmbed) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
|
||||
import "../../src/types";
|
||||
|
||||
test.describe("Embed Pages", () => {
|
||||
test("Event Type Page: should not have margin top on embed page", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/free/30min/embed");
|
||||
@@ -45,18 +47,77 @@ test.describe("Embed Pages", () => {
|
||||
expect(marginFromTop).not.toBe(0);
|
||||
});
|
||||
|
||||
test("should change to embed when window.name is changed to cal-embed=", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/free/30min");
|
||||
test.describe("isEmbed, getEmbedNamespace, getEmbedTheme testing", () => {
|
||||
test("when `window.name` is set to 'cal-embed=' and `theme` is supplied as a query param", async ({
|
||||
page,
|
||||
}) => {
|
||||
const queryParamTheme = "dark";
|
||||
await page.evaluate(() => {
|
||||
window.name = "cal-embed=";
|
||||
});
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.name = "cal-embed=";
|
||||
await page.goto(`http://localhost:3000/free/30min?theme=${queryParamTheme}`);
|
||||
|
||||
const isEmbed = await page.evaluate(() => {
|
||||
return window?.isEmbed?.();
|
||||
});
|
||||
|
||||
const embedNamespace = await page.evaluate(() => {
|
||||
return window?.getEmbedNamespace?.();
|
||||
});
|
||||
|
||||
expect(embedNamespace).toBe("");
|
||||
expect(isEmbed).toBe(true);
|
||||
const embedTheme = await page.evaluate(() => {
|
||||
return window?.getEmbedTheme?.();
|
||||
});
|
||||
expect(embedTheme).toBe(queryParamTheme);
|
||||
const embedStoreTheme = await page.evaluate(() => {
|
||||
return window.CalEmbed.embedStore.theme;
|
||||
});
|
||||
// Verify that the theme is set on embedStore.
|
||||
expect(embedStoreTheme).toBe(queryParamTheme);
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
const isEmbed = await page.evaluate(() => {
|
||||
return window?.isEmbed?.();
|
||||
test("when `window.name` does not contain `cal-embed=`", async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
window.name = "testing";
|
||||
});
|
||||
await page.goto(`http://localhost:3000/free/30min`);
|
||||
const isEmbed = await page.evaluate(() => {
|
||||
return window?.isEmbed?.();
|
||||
});
|
||||
const embedNamespace = await page.evaluate(() => {
|
||||
return window?.getEmbedNamespace?.();
|
||||
});
|
||||
expect(isEmbed).toBe(false);
|
||||
expect(embedNamespace).toBe(null);
|
||||
});
|
||||
|
||||
test("`getEmbedTheme` should use `window.CalEmbed.embedStore.theme` instead of `theme` query param if set", async ({
|
||||
page,
|
||||
}) => {
|
||||
const theme = "dark";
|
||||
await page.evaluate(() => {
|
||||
window.name = "cal-embed=";
|
||||
});
|
||||
await page.goto("http://localhost:3000/free/30min?theme=dark");
|
||||
let embedTheme = await page.evaluate(() => {
|
||||
return window?.getEmbedTheme?.();
|
||||
});
|
||||
expect(embedTheme).toBe(theme);
|
||||
|
||||
// Fake a scenario where theme query param is lost during navigation
|
||||
await page.evaluate(() => {
|
||||
history.pushState({}, "", "/free/30min");
|
||||
});
|
||||
|
||||
embedTheme = await page.evaluate(() => {
|
||||
return window?.getEmbedTheme?.();
|
||||
});
|
||||
|
||||
// Theme should still remain same as it's read from `window.CalEmbed.embedStore.theme` which is updated by getEmbedTheme itself
|
||||
expect(embedTheme).toBe(theme);
|
||||
});
|
||||
expect(isEmbed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
|
||||
import type { Message } from "./embed";
|
||||
import { sdkActionManager } from "./sdk-event";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
export type BookerLayouts = "month_view" | "week_view" | "column_view";
|
||||
|
||||
export type EmbedThemeConfig = Theme | "auto";
|
||||
export type UiConfig = {
|
||||
hideEventTypeDetails?: boolean;
|
||||
// If theme not provided we would get null
|
||||
theme?: EmbedThemeConfig | null;
|
||||
styles?: EmbedStyles & EmbedNonStylesConfig;
|
||||
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
|
||||
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
|
||||
layout?: BookerLayouts;
|
||||
colorScheme?: string | null;
|
||||
};
|
||||
import type { EmbedThemeConfig, UiConfig, EmbedNonStylesConfig, BookerLayouts, EmbedStyles } from "./types";
|
||||
|
||||
type SetStyles = React.Dispatch<React.SetStateAction<EmbedStyles>>;
|
||||
type setNonStylesConfig = React.Dispatch<React.SetStateAction<EmbedNonStylesConfig>>;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CalEmbed: {
|
||||
__logQueue?: unknown[];
|
||||
embedStore: typeof embedStore;
|
||||
applyCssVars: (cssVarsPerTheme: UiConfig["cssVarsPerTheme"]) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This is in-memory persistence needed so that when user browses through the embed, the configurations from the instructions aren't lost.
|
||||
*/
|
||||
@@ -47,20 +40,6 @@ const embedStore = {
|
||||
setUiConfig: [] as ((arg0: UiConfig) => void)[],
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CalEmbed: {
|
||||
__logQueue?: unknown[];
|
||||
embedStore: typeof embedStore;
|
||||
applyCssVars: (cssVarsPerTheme: UiConfig["cssVarsPerTheme"]) => void;
|
||||
};
|
||||
CalComPageStatus: string;
|
||||
isEmbed?: () => boolean;
|
||||
getEmbedNamespace: () => string | null;
|
||||
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||
}
|
||||
}
|
||||
|
||||
let isSafariBrowser = false;
|
||||
const isBrowser = typeof window !== "undefined";
|
||||
|
||||
@@ -100,23 +79,6 @@ function log(...args: unknown[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
|
||||
// Keep this list to minimum, only adding those styles which are really needed.
|
||||
interface EmbedStyles {
|
||||
body?: Pick<CSSProperties, "background">;
|
||||
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
availabilityDatePicker?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
}
|
||||
interface EmbedNonStylesConfig {
|
||||
/** Default would be center */
|
||||
align?: "left";
|
||||
branding?: {
|
||||
brandColor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const setEmbedStyles = (stylesConfig: EmbedStyles) => {
|
||||
embedStore.styles = stylesConfig;
|
||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||
|
||||
@@ -2,17 +2,12 @@
|
||||
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
||||
import { Inline } from "./Inline/inline";
|
||||
import { ModalBox } from "./ModalBox/ModalBox";
|
||||
import type {
|
||||
InterfaceWithParent,
|
||||
interfaceWithParent,
|
||||
UiConfig,
|
||||
EmbedThemeConfig,
|
||||
BookerLayouts,
|
||||
} from "./embed-iframe";
|
||||
import type { InterfaceWithParent, interfaceWithParent } from "./embed-iframe";
|
||||
import css from "./embed.css";
|
||||
import { SdkActionManager } from "./sdk-action-manager";
|
||||
import type { EventData, EventDataMap } from "./sdk-action-manager";
|
||||
import allCss from "./tailwind.generated.css?inline";
|
||||
import type { UiConfig, EmbedThemeConfig, BookerLayouts } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
export type EmbedThemeConfig = Theme | "auto";
|
||||
|
||||
export type BookerLayouts = "month_view" | "week_view" | "column_view";
|
||||
// Only allow certain styles to be modified so that when we make any changes to HTML, we know what all embed styles might be impacted.
|
||||
// Keep this list to minimum, only adding those styles which are really needed.
|
||||
export interface EmbedStyles {
|
||||
body?: Pick<CSSProperties, "background">;
|
||||
eventTypeListItem?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
enabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
disabledDateButton?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
availabilityDatePicker?: Pick<CSSProperties, "background" | "color" | "backgroundColor">;
|
||||
}
|
||||
|
||||
export interface EmbedNonStylesConfig {
|
||||
/** Default would be center */
|
||||
align?: "left";
|
||||
branding?: {
|
||||
brandColor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type UiConfig = {
|
||||
hideEventTypeDetails?: boolean;
|
||||
// If theme not provided we would get null
|
||||
theme?: EmbedThemeConfig | null;
|
||||
styles?: EmbedStyles & EmbedNonStylesConfig;
|
||||
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
|
||||
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
|
||||
layout?: BookerLayouts;
|
||||
colorScheme?: string | null;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
CalComPageStatus: string;
|
||||
isEmbed?: () => boolean;
|
||||
getEmbedNamespace: () => string | null;
|
||||
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -14,9 +14,10 @@ const api = getCalApi();
|
||||
test("Check that the API is available", async () => {
|
||||
expect(api).toBeDefined()
|
||||
const awaitedApi = await api;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
awaitedApi('floatingButton', {
|
||||
calLink: 'free',
|
||||
config: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error We are intentionaly testing invalid value
|
||||
layout: 'wrongview'
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import { handleRefundError } from "@calcom/lib/payment/handleRefundError";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, MembershipRole, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
||||
@@ -44,7 +45,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true, // Not leaking at the moment, be careful with
|
||||
credentials: { select: credentialForCalendarServiceSelect }, // Not leaking at the moment, be careful with
|
||||
email: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
@@ -434,6 +435,7 @@ async function handler(req: CustomRequest) {
|
||||
where: {
|
||||
id: credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (foundCalendarCredential) {
|
||||
calendarCredential = foundCalendarCredential;
|
||||
@@ -725,6 +727,7 @@ async function handleSeatedEventCancellation(
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (credential) {
|
||||
@@ -733,13 +736,7 @@ async function handleSeatedEventCancellation(
|
||||
attendees: evt.attendees.filter((evtAttendee) => attendee.email !== evtAttendee.email),
|
||||
};
|
||||
if (reference.type.includes("_video")) {
|
||||
integrationsToUpdate.push(
|
||||
updateMeeting(
|
||||
{ ...credential, appName: evt.location?.replace("integrations:", "") || "" },
|
||||
updatedEvt,
|
||||
reference
|
||||
)
|
||||
);
|
||||
integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference));
|
||||
}
|
||||
if (reference.type.includes("_calendar")) {
|
||||
const calendar = await getCalendar(credential);
|
||||
|
||||
@@ -470,6 +470,128 @@ describe.sequential("handleNewBooking", () => {
|
||||
},
|
||||
timeout
|
||||
);
|
||||
test(
|
||||
`should create a successful booking when location is provided as label of an option(Done for Organizer Address)
|
||||
1. Should create a booking in the database
|
||||
2. Should send emails to the booker as well as organizer
|
||||
3. Should trigger BOOKING_CREATED webhook
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
webhooks: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
eventTriggers: ["BOOKING_CREATED"],
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
active: true,
|
||||
eventTypeId: 1,
|
||||
appId: null,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar");
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
expect(createdBooking).toContain({
|
||||
location: "New York",
|
||||
});
|
||||
|
||||
expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: mockBookingData.eventTypeId,
|
||||
},
|
||||
},
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
const testEmails = emails.get();
|
||||
expect(testEmails[0]).toHaveEmail({
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
to: `${organizer.email}`,
|
||||
});
|
||||
expect(testEmails[1]).toHaveEmail({
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
});
|
||||
expect(testEmails[1].html).toContain("<title>confirmed_event_type_subject</title>");
|
||||
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
|
||||
triggerEvent: "BOOKING_CREATED",
|
||||
payload: {
|
||||
metadata: {
|
||||
},
|
||||
responses: {
|
||||
name: { label: "your_name", value: "Booker" },
|
||||
email: { label: "email_address", value: "booker@example.com" },
|
||||
location: {
|
||||
label: "location",
|
||||
value: { optionValue: "", value: "New York" },
|
||||
},
|
||||
title: { label: "what_is_this_meeting_about" },
|
||||
notes: { label: "additional_notes" },
|
||||
guests: { label: "additional_guests" },
|
||||
rescheduleReason: { label: "reason_for_reschedule" },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { App, Attendee, Credential, EventTypeCustomInput, DestinationCalendar } from "@prisma/client";
|
||||
import type { App, Attendee, DestinationCalendar, EventTypeCustomInput } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import async from "async";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
@@ -69,6 +69,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { userSelect } from "@calcom/prisma";
|
||||
import type { BookingReference } from "@calcom/prisma/client";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import {
|
||||
bookingCreateBodySchemaForApi,
|
||||
bookingCreateSchemaLegacyPropsForApi,
|
||||
@@ -85,6 +86,7 @@ import type {
|
||||
IntervalLimit,
|
||||
Person,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
@@ -111,11 +113,12 @@ interface IEventTypePaymentCredentialType {
|
||||
*
|
||||
* @param credential
|
||||
*/
|
||||
async function refreshCredential(credential: Credential): Promise<Credential> {
|
||||
async function refreshCredential(credential: CredentialPayload): Promise<CredentialPayload> {
|
||||
const newCredential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!newCredential) {
|
||||
@@ -130,7 +133,7 @@ async function refreshCredential(credential: Credential): Promise<Credential> {
|
||||
*
|
||||
* @param credentials
|
||||
*/
|
||||
async function refreshCredentials(credentials: Array<Credential>): Promise<Array<Credential>> {
|
||||
async function refreshCredentials(credentials: Array<CredentialPayload>): Promise<Array<CredentialPayload>> {
|
||||
return await async.mapLimit(credentials, 5, refreshCredential);
|
||||
}
|
||||
|
||||
@@ -139,7 +142,7 @@ async function refreshCredentials(credentials: Array<Credential>): Promise<Array
|
||||
*
|
||||
*/
|
||||
const getAllCredentials = async (
|
||||
user: User & { credentials: Credential[] },
|
||||
user: User & { credentials: CredentialPayload[] },
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
|
||||
) => {
|
||||
const allCredentials = user.credentials;
|
||||
@@ -150,6 +153,7 @@ const getAllCredentials = async (
|
||||
where: {
|
||||
teamId: eventType.team.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
allCredentials.push(...teamCredentialsQuery);
|
||||
}
|
||||
@@ -165,7 +169,9 @@ const getAllCredentials = async (
|
||||
},
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (teamCredentialsQuery?.credentials) {
|
||||
@@ -180,7 +186,9 @@ const getAllCredentials = async (
|
||||
id: user.organizationId,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -241,7 +249,9 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
disableGuests: true,
|
||||
users: {
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
...userSelect.select,
|
||||
},
|
||||
},
|
||||
@@ -317,7 +327,9 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
isFixed: true,
|
||||
user: {
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
...userSelect.select,
|
||||
organization: {
|
||||
select: {
|
||||
@@ -352,7 +364,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
|
||||
type IsFixedAwareUser = User & {
|
||||
isFixed: boolean;
|
||||
credentials: Credential[];
|
||||
credentials: CredentialPayload[];
|
||||
organization: { slug: string };
|
||||
};
|
||||
|
||||
@@ -367,6 +379,7 @@ async function ensureAvailableUsers(
|
||||
}
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, 'minute');
|
||||
|
||||
const originalBookingDuration = input.originalRescheduledBooking
|
||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||
@@ -411,10 +424,10 @@ async function ensureAvailableUsers(
|
||||
// running at the first unavailable time.
|
||||
let i = 0;
|
||||
while (!foundConflict && i < allBookingDates.length) {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], eventType.length);
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], duration);
|
||||
}
|
||||
} else {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, eventType.length);
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
||||
}
|
||||
} catch {
|
||||
log.debug({
|
||||
@@ -735,7 +748,9 @@ async function handler(
|
||||
},
|
||||
select: {
|
||||
...userSelect.select,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
@@ -785,7 +800,9 @@ async function handler(
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
credentials: true, // Don't leak to client
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
}, // Don't leak to client
|
||||
...userSelect.select,
|
||||
},
|
||||
});
|
||||
@@ -994,6 +1011,7 @@ async function handler(
|
||||
teamDestinationCalendars.push(user.destinationCalendar);
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email ?? "",
|
||||
name: user.name ?? "",
|
||||
firstName: "",
|
||||
@@ -1111,6 +1129,7 @@ async function handler(
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (credential) {
|
||||
@@ -1722,6 +1741,7 @@ async function handler(
|
||||
eventType,
|
||||
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
|
||||
booking,
|
||||
fullName,
|
||||
bookerEmail
|
||||
);
|
||||
|
||||
@@ -1911,7 +1931,7 @@ async function handler(
|
||||
const createBookingObj = {
|
||||
include: {
|
||||
user: {
|
||||
select: { email: true, name: true, timeZone: true },
|
||||
select: { email: true, name: true, timeZone: true, username: true },
|
||||
},
|
||||
attendees: true,
|
||||
payment: true,
|
||||
@@ -2279,6 +2299,7 @@ async function handler(
|
||||
eventType,
|
||||
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
|
||||
booking,
|
||||
fullName,
|
||||
bookerEmail
|
||||
);
|
||||
|
||||
@@ -2466,6 +2487,7 @@ const findBookingQuery = async (bookingId: number) => {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export const config = {
|
||||
@@ -74,7 +75,9 @@ export async function handlePaymentSuccess(
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
timeZone: true,
|
||||
email: true,
|
||||
name: true,
|
||||
@@ -297,7 +300,7 @@ export const findPaymentCredentials = async (
|
||||
): Promise<{ clientId: string; secretKey: string; webhookId: string }> => {
|
||||
try {
|
||||
// @TODO: what about team bookings with paypal?
|
||||
const userFromBooking = await prisma?.booking.findFirst({
|
||||
const userFromBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
@@ -309,7 +312,7 @@ export const findPaymentCredentials = async (
|
||||
|
||||
if (!userFromBooking) throw new Error("No user found");
|
||||
|
||||
const credentials = await prisma?.credential.findFirst({
|
||||
const credentials = await prisma.credential.findFirst({
|
||||
where: {
|
||||
appId: "paypal",
|
||||
userId: userFromBooking?.userId,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type Stripe from "stripe";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendScheduledEmails, sendOrganizerRequestEmail, sendAttendeeRequestEmail } from "@calcom/emails";
|
||||
import { sendAttendeeRequestEmail, sendOrganizerRequestEmail, sendScheduledEmails } from "@calcom/emails";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
@@ -15,8 +15,9 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { prisma, bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { bookingMinimalSelect, prisma } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -164,7 +165,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
credentials: true,
|
||||
credentials: { select: credentialForCalendarServiceSelect },
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
email: true,
|
||||
@@ -313,7 +314,7 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
|
||||
name: true,
|
||||
locale: true,
|
||||
destinationCalendar: true,
|
||||
credentials: true,
|
||||
credentials: { select: credentialForCalendarServiceSelect },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
|
||||
export const isSAMLLoginEnabled = samlDatabaseUrl.length > 0;
|
||||
@@ -30,38 +28,6 @@ export const isSAMLAdmin = (email: string) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const samlTenantProduct = async (prisma: PrismaClient, email: string) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
invitedTo: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "no_account_exists",
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.invitedTo) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Could not find a SAML Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenant: tenantPrefix + user.invitedTo,
|
||||
product: samlProductID,
|
||||
};
|
||||
};
|
||||
|
||||
export const canAccess = async (user: { id: number; email: string }, teamId: number | null) => {
|
||||
const { id: userId, email } = user;
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
import jackson from "./jackson";
|
||||
import { tenantPrefix, samlProductID } from "./saml";
|
||||
|
||||
export const ssoTenantProduct = async (prisma: PrismaClient, email: string) => {
|
||||
const { connectionController } = await jackson();
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
where: {
|
||||
accepted: true,
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!memberships || memberships.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "no_account_exists",
|
||||
});
|
||||
}
|
||||
|
||||
// Check SSO connections for each team user is a member of
|
||||
// We'll use the first one we find
|
||||
const promises = memberships.map(({ teamId }) =>
|
||||
connectionController.getConnections({
|
||||
tenant: `${tenantPrefix}${teamId}`,
|
||||
product: samlProductID,
|
||||
})
|
||||
);
|
||||
|
||||
const connectionResults = await Promise.allSettled(promises);
|
||||
|
||||
const connectionsFound = connectionResults
|
||||
.filter((result) => result.status === "fulfilled")
|
||||
.map((result) => (result.status === "fulfilled" ? result.value : []))
|
||||
.filter((connections) => connections.length > 0);
|
||||
|
||||
if (connectionsFound.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Could not find a SSO Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tenant: connectionsFound[0][0].tenant,
|
||||
product: samlProductID,
|
||||
};
|
||||
};
|
||||
@@ -240,9 +240,9 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
|
||||
index !== 0 && "border-subtle border-t"
|
||||
)}
|
||||
data-testid="pending-member-item">
|
||||
<div className="flex space-x-2 rtl:space-x-reverse">
|
||||
<div className="mr-4 flex max-w-full space-x-2 overflow-hidden rtl:space-x-reverse">
|
||||
<Avatar size="mdLg" imageSrc={bookerUrl + "/" + member.username + "/avatar.png"} alt="owner-avatar" />
|
||||
<div>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
<div className="flex space-x-1">
|
||||
<p>{member.name || member.email || t("team_member")}</p>
|
||||
{/* Assume that the first member of the team is the creator */}
|
||||
@@ -252,9 +252,9 @@ const PendingMemberItem = (props: { member: TeamMember; index: number; teamId: n
|
||||
{member.role === "ADMIN" && <Badge variant="default">{t("admin")}</Badge>}
|
||||
</div>
|
||||
{member.username ? (
|
||||
<p className="text-default">{`${WEBAPP_URL}/${member.username}`}</p>
|
||||
<p className="text-default truncate">{`${WEBAPP_URL}/${member.username}`}</p>
|
||||
) : (
|
||||
<p className="text-default">{t("not_on_cal", { appName: APP_NAME })}</p>
|
||||
<p className="text-default truncate">{t("not_on_cal", { appName: APP_NAME })}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,14 +48,27 @@ const customTemplate = (
|
||||
|
||||
const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR;
|
||||
|
||||
const attendeeNameWords = variables.attendeeName?.trim().split(" ");
|
||||
const attendeeNameWordCount = attendeeNameWords?.length ?? 0;
|
||||
|
||||
const attendeeFirstName = variables.attendeeFirstName
|
||||
? variables.attendeeFirstName
|
||||
: attendeeNameWords?.[0] ?? "";
|
||||
|
||||
const attendeeLastName = variables.attendeeLastName
|
||||
? variables.attendeeLastName
|
||||
: attendeeNameWordCount > 1
|
||||
? attendeeNameWords![attendeeNameWordCount - 1]
|
||||
: "";
|
||||
|
||||
let dynamicText = text
|
||||
.replaceAll("{EVENT_NAME}", variables.eventName || "")
|
||||
.replaceAll("{ORGANIZER}", variables.organizerName || "")
|
||||
.replaceAll("{ATTENDEE}", variables.attendeeName || "")
|
||||
.replaceAll("{ORGANIZER_NAME}", variables.organizerName || "") //old variable names
|
||||
.replaceAll("{ATTENDEE_NAME}", variables.attendeeName || "") //old variable names
|
||||
.replaceAll("{ATTENDEE_FIRST_NAME}", variables.attendeeFirstName || "")
|
||||
.replaceAll("{ATTENDEE_LAST_NAME}", variables.attendeeLastName || "")
|
||||
.replaceAll("{ATTENDEE_FIRST_NAME}", attendeeFirstName)
|
||||
.replaceAll("{ATTENDEE_LAST_NAME}", attendeeLastName)
|
||||
.replaceAll("{EVENT_DATE}", translatedDate)
|
||||
.replaceAll("{EVENT_TIME}", variables.eventDate?.format(currentTimeFormat) || "")
|
||||
.replaceAll("{START_TIME}", variables.eventDate?.format(currentTimeFormat) || "")
|
||||
|
||||
@@ -311,6 +311,10 @@ export const ComponentForField = ({
|
||||
if (!field.optionsInputs) {
|
||||
throw new Error("Field optionsInputs is not defined");
|
||||
}
|
||||
const options = field.options.map((field) => {
|
||||
return { ...field, value: field.value === "inPerson" ? field.label : field.value };
|
||||
});
|
||||
|
||||
return field.options.length ? (
|
||||
<WithLabel field={field} readOnly={readOnly}>
|
||||
<componentConfig.factory
|
||||
@@ -320,7 +324,7 @@ export const ComponentForField = ({
|
||||
value={value as { value: string; optionValue: string }}
|
||||
setValue={setValue as (arg: typeof value) => void}
|
||||
optionsInputs={field.optionsInputs}
|
||||
options={field.options}
|
||||
options={options}
|
||||
required={field.required}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const TotalBookingUsersTable = ({
|
||||
<Avatar
|
||||
alt={item.user.name || ""}
|
||||
size="sm"
|
||||
imageSrc={item.user.avatar}
|
||||
imageSrc={"/" + item.user.username + "/avatar.png"}
|
||||
title={item.user.name || ""}
|
||||
className="m-2"
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,6 @@ const UserSelect = {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
username: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -43,11 +43,13 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
|
||||
weekday,
|
||||
control,
|
||||
CopyButton,
|
||||
disabled,
|
||||
}: {
|
||||
name: ArrayPath<TFieldValues>;
|
||||
weekday: string;
|
||||
control: Control<TFieldValues>;
|
||||
CopyButton: JSX.Element;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { watch, setValue } = useFormContext();
|
||||
const watchDayRange = watch(name);
|
||||
@@ -60,7 +62,7 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
|
||||
<label className="text-default flex flex-row items-center space-x-2 rtl:space-x-reverse">
|
||||
<div>
|
||||
<Switch
|
||||
disabled={!watchDayRange}
|
||||
disabled={!watchDayRange || disabled}
|
||||
defaultChecked={watchDayRange && watchDayRange.length > 0}
|
||||
checked={watchDayRange && !!watchDayRange.length}
|
||||
onCheckedChange={(isChecked) => {
|
||||
@@ -75,8 +77,8 @@ const ScheduleDay = <TFieldValues extends FieldValues>({
|
||||
<>
|
||||
{watchDayRange ? (
|
||||
<div className="flex sm:ml-2">
|
||||
<DayRanges control={control} name={name} />
|
||||
{!!watchDayRange.length && <div className="block">{CopyButton}</div>}
|
||||
<DayRanges control={control} name={name} disabled={disabled} />
|
||||
{!!watchDayRange.length && !disabled && <div className="block">{CopyButton}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<SkeletonText className="ml-1 mt-2.5 h-6 w-48" />
|
||||
@@ -133,11 +135,13 @@ const Schedule = <
|
||||
>({
|
||||
name,
|
||||
control,
|
||||
disabled,
|
||||
weekStart = 0,
|
||||
}: {
|
||||
name: TPath;
|
||||
control: Control<TFieldValues>;
|
||||
weekStart?: number;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { i18n } = useLocale();
|
||||
|
||||
@@ -149,6 +153,7 @@ const Schedule = <
|
||||
const dayRangeName = `${name}.${weekdayIndex}` as ArrayPath<TFieldValues>;
|
||||
return (
|
||||
<ScheduleDay
|
||||
disabled={disabled}
|
||||
name={dayRangeName}
|
||||
key={weekday}
|
||||
weekday={weekday}
|
||||
@@ -163,10 +168,12 @@ const Schedule = <
|
||||
|
||||
export const DayRanges = <TFieldValues extends FieldValues>({
|
||||
name,
|
||||
disabled,
|
||||
control,
|
||||
}: {
|
||||
name: ArrayPath<TFieldValues>;
|
||||
control?: Control<TFieldValues>;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { getValues } = useFormContext();
|
||||
@@ -184,6 +191,7 @@ export const DayRanges = <TFieldValues extends FieldValues>({
|
||||
<Controller name={`${name}.${index}`} render={({ field }) => <TimeRangeField {...field} />} />
|
||||
{index === 0 && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
tooltip={t("add_time_availability")}
|
||||
className="text-default mx-2 "
|
||||
type="button"
|
||||
@@ -220,15 +228,18 @@ export const DayRanges = <TFieldValues extends FieldValues>({
|
||||
const RemoveTimeButton = ({
|
||||
index,
|
||||
remove,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
index: number | number[];
|
||||
remove: UseFieldArrayRemove;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
@@ -240,12 +251,18 @@ const RemoveTimeButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
|
||||
const TimeRangeField = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: { className?: string; disabled?: boolean } & ControllerRenderProps) => {
|
||||
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
|
||||
return (
|
||||
<div className={classNames("flex flex-row gap-1", className)}>
|
||||
<LazySelect
|
||||
className="inline-block w-[100px]"
|
||||
isDisabled={disabled}
|
||||
value={value.start}
|
||||
max={value.end}
|
||||
onChange={(option) => {
|
||||
@@ -255,6 +272,7 @@ const TimeRangeField = ({ className, value, onChange }: { className?: string } &
|
||||
<span className="text-default mx-2 w-2 self-center"> - </span>
|
||||
<LazySelect
|
||||
className="inline-block w-[100px] rounded-md"
|
||||
isDisabled={disabled}
|
||||
value={value.end}
|
||||
min={value.start}
|
||||
onChange={(option) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -16,7 +17,6 @@ import type { VerticalTabItemProps } from "@calcom/ui";
|
||||
import { Badge, Button, ErrorBoundary, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CreditCard,
|
||||
@@ -76,7 +76,6 @@ const tabs: VerticalTabItemProps[] = [
|
||||
{
|
||||
name: "organization",
|
||||
href: "/settings/organizations",
|
||||
icon: Building,
|
||||
children: [
|
||||
{
|
||||
name: "profile",
|
||||
@@ -137,6 +136,7 @@ const organizationRequiredKeys = ["organization"];
|
||||
const useTabs = () => {
|
||||
const session = useSession();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
|
||||
|
||||
@@ -144,7 +144,10 @@ const useTabs = () => {
|
||||
if (tab.href === "/settings/my-account") {
|
||||
tab.name = user?.name || "my_account";
|
||||
tab.icon = undefined;
|
||||
tab.avatar = WEBAPP_URL + "/" + session?.data?.user?.username + "/avatar.png";
|
||||
tab.avatar = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${session?.data?.user?.username}/avatar.png`;
|
||||
} else if (tab.href === "/settings/organizations") {
|
||||
tab.name = orgBranding?.name || "organization";
|
||||
tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`;
|
||||
} else if (
|
||||
tab.href === "/settings/security" &&
|
||||
user?.identityProvider === IdentityProvider.GOOGLE &&
|
||||
|
||||
@@ -833,7 +833,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Avatar
|
||||
alt={`${orgBranding.name} logo`}
|
||||
imageSrc={`${orgBranding.fullDomain}/avatar.png`}
|
||||
imageSrc={`${orgBranding.fullDomain}/org/${orgBranding.slug}/avatar.png`}
|
||||
size="xsm"
|
||||
/>
|
||||
<p className="text line-clamp-1 text-sm">
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Label,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
TimezoneSelect,
|
||||
showToast,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { SliderUser } from "./AvailabilitySliderTable";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedUser?: SliderUser | null;
|
||||
}
|
||||
|
||||
type AvailabilityFormValues = {
|
||||
name: string;
|
||||
schedule: ScheduleType;
|
||||
dateOverrides: { ranges: TimeRange[] }[];
|
||||
timeZone: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]; disabled?: boolean }) => {
|
||||
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
name: "dateOverrides",
|
||||
});
|
||||
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="">
|
||||
<Label>{t("date_overrides")}</Label>
|
||||
<div className="space-y-2">
|
||||
<DateOverrideList
|
||||
excludedDates={excludedDates}
|
||||
remove={remove}
|
||||
replace={replace}
|
||||
items={fields}
|
||||
workingHours={workingHours}
|
||||
/>
|
||||
<DateOverrideInputDialog
|
||||
workingHours={workingHours}
|
||||
excludedDates={excludedDates}
|
||||
onChange={(ranges) => ranges.forEach((range) => append({ ranges: [range] }))}
|
||||
Trigger={
|
||||
<Button color="secondary" StartIcon={Plus} data-testid="add-override" disabled={disabled}>
|
||||
{t("add_an_override")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function AvailabilityEditSheet(props: Props) {
|
||||
// This sheet will not be rendered without a selected user
|
||||
const userId = props.selectedUser?.id as number;
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { data: hasEditPermission, isLoading: loadingPermissions } =
|
||||
trpc.viewer.teams.hasEditPermissionForUser.useQuery({
|
||||
memberId: userId,
|
||||
});
|
||||
|
||||
const { data, isLoading } = trpc.viewer.availability.schedule.getScheduleByUserId.useQuery({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
utils.viewer.availability.listTeam.invalidate();
|
||||
showToast(t("success"), "success");
|
||||
props.onOpenChange(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<AvailabilityFormValues>({
|
||||
values: data && {
|
||||
...data,
|
||||
timeZone: data?.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
schedule: data?.availability || [],
|
||||
},
|
||||
});
|
||||
|
||||
const watchTimezone = form.watch("timeZone");
|
||||
|
||||
return (
|
||||
<Sheet open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||
// Just blocking on a UI side -> Backend will also do the validation
|
||||
if (!hasEditPermission) return;
|
||||
data &&
|
||||
updateMutation.mutate({
|
||||
scheduleId: data?.id,
|
||||
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||
...values,
|
||||
});
|
||||
}}>
|
||||
<SheetContent
|
||||
bottomActions={
|
||||
<>
|
||||
<Button color="secondary" className="w-full justify-center">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
|
||||
className="w-full justify-center"
|
||||
type="submit"
|
||||
loading={updateMutation.isLoading}
|
||||
form="availability-form">
|
||||
{t("save")}
|
||||
</Button>
|
||||
</>
|
||||
}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t("edit_users_availability", {
|
||||
username: props.selectedUser?.username ?? "Nameless user",
|
||||
})}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
{!data?.hasDefaultSchedule && !isLoading && hasEditPermission && (
|
||||
<div className="my-2">
|
||||
<Alert severity="warning" title={t("view_only_edit_availability_not_onboarded")} />
|
||||
</div>
|
||||
)}
|
||||
{!hasEditPermission && !loadingPermissions && (
|
||||
<div className="my-2">
|
||||
<Alert severity="warning" title={t("view_only_edit_availability")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex flex-col space-y-4">
|
||||
<div>
|
||||
<Label className="text-emphasis">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
isDisabled={!hasEditPermission || !data?.hasDefaultSchedule}
|
||||
value={watchTimezone ?? "Europe/London"}
|
||||
onChange={(event) => {
|
||||
if (event) form.setValue("timeZone", event.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Label className="text-emphasis">{t("members_default_schedule")}</Label>
|
||||
{/* Remove padding from schedule without touching the component */}
|
||||
<div className="[&>*:first-child]:!p-0">
|
||||
<Schedule
|
||||
control={form.control}
|
||||
name="schedule"
|
||||
weekStart={0}
|
||||
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{data?.workingHours && (
|
||||
<DateOverride
|
||||
workingHours={data.workingHours}
|
||||
disabled={!hasEditPermission || !data.hasDefaultSchedule}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Form>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
|
||||
|
||||
import { UpgradeTip } from "../../tips/UpgradeTip";
|
||||
import { TBContext, createTimezoneBuddyStore } from "../store";
|
||||
import { AvailabilityEditSheet } from "./AvailabilityEditSheet";
|
||||
import { TimeDial } from "./TimeDial";
|
||||
|
||||
export interface SliderUser {
|
||||
@@ -20,6 +21,7 @@ export interface SliderUser {
|
||||
email: string;
|
||||
timeZone: string;
|
||||
role: MembershipRole;
|
||||
defaultScheduleId: number | null;
|
||||
dateRanges: DateRange[];
|
||||
}
|
||||
|
||||
@@ -50,8 +52,11 @@ function UpgradeTeamTip() {
|
||||
}
|
||||
|
||||
export function AvailabilitySliderTable() {
|
||||
const { t } = useLocale();
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [browsingDate, setBrowsingDate] = useState(dayjs());
|
||||
const [editSheetOpen, setEditSheetOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<SliderUser | null>(null);
|
||||
|
||||
const { data, isLoading, fetchNextPage, isFetching } = trpc.viewer.availability.listTeam.useInfiniteQuery(
|
||||
{
|
||||
@@ -175,16 +180,32 @@ export function AvailabilitySliderTable() {
|
||||
value={createTimezoneBuddyStore({
|
||||
browsingDate: browsingDate.toDate(),
|
||||
})}>
|
||||
<div className="relative">
|
||||
<DataTable
|
||||
tableContainerRef={tableContainerRef}
|
||||
columns={memorisedColumns}
|
||||
data={flatData}
|
||||
isLoading={isLoading}
|
||||
// tableOverlay={<HoverOverview />}
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="relative">
|
||||
<DataTable
|
||||
tableContainerRef={tableContainerRef}
|
||||
columns={memorisedColumns}
|
||||
onRowMouseclick={(row) => {
|
||||
setEditSheetOpen(true);
|
||||
setSelectedUser(row.original);
|
||||
}}
|
||||
data={flatData}
|
||||
isLoading={isLoading}
|
||||
// tableOverlay={<HoverOverview />}
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
|
||||
/>
|
||||
</div>
|
||||
{selectedUser && editSheetOpen ? (
|
||||
<AvailabilityEditSheet
|
||||
open={editSheetOpen}
|
||||
onOpenChange={(e) => {
|
||||
setEditSheetOpen(e);
|
||||
setSelectedUser(null); // We need to clear the user here or else the sheet will not re-render when opening a new user
|
||||
}}
|
||||
selectedUser={selectedUser}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</TBContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ const getDuration = (start: string, end: string): DurationObject => ({
|
||||
minutes: dayjs(end).diff(dayjs(start), "minute"),
|
||||
});
|
||||
|
||||
const getAttendees = (attendees: Person[]): Attendee[] =>
|
||||
const mapAttendees = (attendees: Person[]): Attendee[] =>
|
||||
attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));
|
||||
|
||||
export default abstract class BaseCalendarService implements Calendar {
|
||||
@@ -104,6 +104,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
private headers: Record<string, string> = {};
|
||||
protected integrationName = "";
|
||||
private log: typeof logger;
|
||||
private credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload, integrationName: string, url?: string) {
|
||||
this.integrationName = integrationName;
|
||||
@@ -118,11 +119,25 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
|
||||
this.credentials = { username, password };
|
||||
this.headers = getBasicAuthHeaders({ username, password });
|
||||
this.credential = credential;
|
||||
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
private getAttendees(event: CalendarEvent) {
|
||||
const attendees = mapAttendees(event.attendees);
|
||||
|
||||
if (event.team?.members) {
|
||||
const teamAttendeesWithoutCurrentUser = event.team.members.filter(
|
||||
(member) => member.email !== this.credential.user?.email
|
||||
);
|
||||
attendees.push(...mapAttendees(teamAttendeesWithoutCurrentUser));
|
||||
}
|
||||
|
||||
return attendees;
|
||||
}
|
||||
|
||||
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||
try {
|
||||
const calendars = await this.listCalendars(event);
|
||||
|
||||
@@ -138,10 +153,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
description: getRichDescription(event),
|
||||
location: getLocation(event),
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: [
|
||||
...getAttendees(event.attendees),
|
||||
...(event.team?.members ? getAttendees(event.team.members) : []),
|
||||
],
|
||||
attendees: this.getAttendees(event),
|
||||
/** according to https://datatracker.ietf.org/doc/html/rfc2446#section-3.2.1, in a published iCalendar component.
|
||||
* "Attendees" MUST NOT be present
|
||||
* `attendees: this.getAttendees(event.attendees),`
|
||||
@@ -153,7 +165,10 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
if (error || !iCalString)
|
||||
throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `);
|
||||
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const mainHostDestinationCalendar = event.destinationCalendar
|
||||
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
|
||||
event.destinationCalendar[0]
|
||||
: undefined;
|
||||
|
||||
// We create the event directly on iCal
|
||||
const responses = await Promise.all(
|
||||
@@ -214,10 +229,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||
description: getRichDescription(event),
|
||||
location: getLocation(event),
|
||||
organizer: { email: event.organizer.email, name: event.organizer.name },
|
||||
attendees: [
|
||||
...getAttendees(event.attendees),
|
||||
...(event.team?.members ? getAttendees(event.team.members) : []),
|
||||
],
|
||||
attendees: this.getAttendees(event),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma, Credential } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
@@ -6,6 +6,7 @@ import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { userSelect } from "@calcom/prisma/selects";
|
||||
import type { CustomInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
@@ -25,7 +26,7 @@ type UsernameSlugLinkProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
const user: User & { credentials: Credential[] } = {
|
||||
const user: User & { credentials: CredentialPayload[] } = {
|
||||
metadata: null,
|
||||
theme: null,
|
||||
credentials: [],
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
const ROLES_WITH_EDIT_PERMISSION = [MembershipRole.ADMIN, MembershipRole.OWNER] as MembershipRole[];
|
||||
|
||||
type InputOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: {
|
||||
memberId: number;
|
||||
};
|
||||
};
|
||||
|
||||
export async function hasEditPermissionForUserID({ ctx, input }: InputOptions) {
|
||||
const { user } = ctx;
|
||||
|
||||
const authedUsersTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
role: {
|
||||
in: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const targetUsersTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: input.memberId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamIdOverlaps = authedUsersTeams.some((authedTeam) => {
|
||||
return targetUsersTeams.some((targetTeam) => targetTeam.teamId === authedTeam.teamId);
|
||||
});
|
||||
|
||||
return teamIdOverlaps;
|
||||
}
|
||||
|
||||
export async function hasReadPermissionsForUserId({ ctx, input }: InputOptions) {
|
||||
const { user } = ctx;
|
||||
|
||||
const authedUsersTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
const targetUsersTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: input.memberId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
const teamIdOverlaps = authedUsersTeams.some((authedTeam) => {
|
||||
return targetUsersTeams.some((targetTeam) => targetTeam.teamId === authedTeam.teamId);
|
||||
});
|
||||
|
||||
return teamIdOverlaps;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentS
|
||||
|
||||
const handlePayment = async (
|
||||
evt: CalendarEvent,
|
||||
selectedEventType: Pick<Zod.infer<typeof EventTypeModel>, "metadata">,
|
||||
selectedEventType: Pick<Zod.infer<typeof EventTypeModel>, "metadata" | "title">,
|
||||
paymentAppCredentials: {
|
||||
key: Prisma.JsonValue;
|
||||
appId: EventTypeAppsList;
|
||||
@@ -18,11 +18,13 @@ const handlePayment = async (
|
||||
} | null;
|
||||
},
|
||||
booking: {
|
||||
user: { email: string | null; name: string | null; timeZone: string } | null;
|
||||
user: { email: string | null; name: string | null; timeZone: string; username: string | null } | null;
|
||||
id: number;
|
||||
userId: number | null;
|
||||
startTime: { toISOString: () => string };
|
||||
uid: string;
|
||||
},
|
||||
bookerName: string,
|
||||
bookerEmail: string
|
||||
) => {
|
||||
const paymentApp = (await appStore[
|
||||
@@ -58,8 +60,12 @@ const handlePayment = async (
|
||||
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
|
||||
},
|
||||
booking.id,
|
||||
booking.userId,
|
||||
booking.user?.username ?? null,
|
||||
bookerName,
|
||||
bookerEmail,
|
||||
paymentOption,
|
||||
selectedEventType.title,
|
||||
evt.title
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
export async function getUsersCredentials(userId: number) {
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
teamId: true,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export const credentialForCalendarServiceSelect = Prisma.validator<Prisma.CredentialSelect>()({
|
||||
id: true,
|
||||
appId: true,
|
||||
type: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
key: true,
|
||||
invalid: true,
|
||||
});
|
||||
|
||||
export const safeCredentialSelect = Prisma.validator<Prisma.CredentialSelect>()({
|
||||
id: true,
|
||||
type: true,
|
||||
/** Omitting to avoid frontend leaks */
|
||||
// key: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DestinationCalendar } from "@prisma/client";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
|
||||
@@ -26,6 +27,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
// get user's credentials + their connected integrations
|
||||
|
||||
@@ -10,9 +10,9 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { deletePayment } from "@calcom/lib/payment/deletePayment";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { bookingMinimalSelect, prisma } from "@calcom/prisma";
|
||||
import { AppCategories, BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
@@ -37,8 +37,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||
...(teamId ? { teamId } : { userId: ctx.user.id }),
|
||||
},
|
||||
select: {
|
||||
key: true,
|
||||
appId: true,
|
||||
...credentialForCalendarServiceSelect,
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
@@ -46,11 +45,6 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||
dirName: true,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
type: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Credential, Prisma } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
@@ -6,7 +6,9 @@ import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import type { TIntegrationsInputSchema } from "./integrations.schema";
|
||||
|
||||
@@ -20,7 +22,9 @@ type IntegrationsOptions = {
|
||||
type TeamQuery = Prisma.TeamGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
credentials?: true;
|
||||
credentials: {
|
||||
select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect;
|
||||
};
|
||||
name: true;
|
||||
logo: true;
|
||||
members: {
|
||||
@@ -63,7 +67,9 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logo: true,
|
||||
members: {
|
||||
@@ -77,7 +83,9 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logo: true,
|
||||
members: {
|
||||
@@ -109,7 +117,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
|
||||
userTeams = [...teamsQuery, ...parentTeams];
|
||||
|
||||
const teamAppCredentials: Credential[] = userTeams.flatMap((teamApp) => {
|
||||
const teamAppCredentials: CredentialPayload[] = userTeams.flatMap((teamApp) => {
|
||||
return teamApp.credentials ? teamApp.credentials.flat() : [];
|
||||
});
|
||||
if (!includeTeamInstalledApps || teamId) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||
@@ -14,7 +14,7 @@ export const samlTenantProductHandler = ({ ctx, input }: SamlTenantProductOption
|
||||
const { prisma } = ctx;
|
||||
const { email } = input;
|
||||
|
||||
return samlTenantProduct(prisma, email);
|
||||
return ssoTenantProduct(prisma, email);
|
||||
};
|
||||
|
||||
export default samlTenantProductHandler;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ZCreateInputSchema } from "./create.schema";
|
||||
import { ZDeleteInputSchema } from "./delete.schema";
|
||||
import { ZScheduleDuplicateSchema } from "./duplicate.schema";
|
||||
import { ZGetInputSchema } from "./get.schema";
|
||||
import { ZGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
|
||||
import { ZUpdateInputSchema } from "./update.schema";
|
||||
|
||||
type ScheduleRouterHandlerCache = {
|
||||
@@ -12,6 +13,7 @@ type ScheduleRouterHandlerCache = {
|
||||
delete?: typeof import("./delete.handler").deleteHandler;
|
||||
update?: typeof import("./update.handler").updateHandler;
|
||||
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
|
||||
getScheduleByUserId?: typeof import("./getScheduleByUserId.handler").getScheduleByUserIdHandler;
|
||||
};
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: ScheduleRouterHandlerCache = {};
|
||||
@@ -98,4 +100,22 @@ export const scheduleRouter = router({
|
||||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
getScheduleByUserId: authedProcedure.input(ZGetByUserIdInputSchema).query(async ({ input, ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getScheduleByUserId) {
|
||||
UNSTABLE_HANDLER_CACHE.getScheduleByUserId = await import("./getScheduleByUserId.handler").then(
|
||||
(mod) => mod.getScheduleByUserIdHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getScheduleByUserId) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getScheduleByUserId({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||
import { hasReadPermissionsForUserId } from "@calcom/lib/hasEditPermissionForUser";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
@@ -30,25 +31,22 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
name: true,
|
||||
availability: true,
|
||||
timeZone: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
eventName: true,
|
||||
team: {
|
||||
select: { members: { select: { userId: true } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isCurrentUserPartOfTeam = schedule?.eventType.some((eventType) =>
|
||||
eventType.team?.members.some((teamMember) => teamMember.userId === user.id)
|
||||
);
|
||||
if (!schedule) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
}
|
||||
const isCurrentUserPartOfTeam = hasReadPermissionsForUserId({
|
||||
ctx,
|
||||
input: { memberId: schedule?.userId },
|
||||
});
|
||||
|
||||
const isCurrentUserOwner = schedule?.userId === user.id;
|
||||
|
||||
if (!schedule || (!isCurrentUserOwner && !isCurrentUserPartOfTeam)) {
|
||||
if (!isCurrentUserPartOfTeam && !isCurrentUserOwner) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
});
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../../trpc";
|
||||
import { getHandler } from "./get.handler";
|
||||
import type { TGetByUserIdInputSchema } from "./getScheduleByUserId.schema";
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TGetByUserIdInputSchema;
|
||||
};
|
||||
|
||||
const EMPTY_SCHEDULE = [[], [], [], [], [], [], []];
|
||||
|
||||
export const getScheduleByUserIdHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const foundUserDefaultId = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
id: input.userId,
|
||||
},
|
||||
select: {
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// This looks kinda weird that we throw straight in the catch - its so that we can return a default schedule if the user has not completed onboarding @shiraz will loveme for this
|
||||
if (!foundUserDefaultId?.defaultScheduleId) {
|
||||
throw new Error("NOT_FOUND");
|
||||
}
|
||||
const schedule = await getHandler({
|
||||
ctx,
|
||||
input: {
|
||||
scheduleId: foundUserDefaultId?.defaultScheduleId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...schedule,
|
||||
hasDefaultSchedule: true,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
id: -1,
|
||||
name: "Working Hourse",
|
||||
availability: EMPTY_SCHEDULE,
|
||||
dateOverrides: [],
|
||||
timeZone: ctx.user.timeZone || "Europe/London",
|
||||
workingHours: [],
|
||||
isDefault: true,
|
||||
hasDefaultSchedule: false, // This is the path that we take if the user has not completed onboarding
|
||||
};
|
||||
}
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZGetByUserIdInputSchema = z.object({
|
||||
userId: z.optional(z.number()),
|
||||
});
|
||||
|
||||
export type TGetByUserIdInputSchema = z.infer<typeof ZGetByUserIdInputSchema>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user