Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bc40a3eb6 | ||
|
|
bd21b2dfe1 | ||
|
|
d60fb081c9 | ||
|
|
197b435b6f | ||
|
|
add297b09a | ||
|
|
d65f0ee0e8 | ||
|
|
59c0a71db7 | ||
|
|
d50151fe08 | ||
|
|
d2b0dd8eab | ||
|
|
bb0574dff8 | ||
|
|
49364eee9a | ||
|
|
adb0530d69 | ||
|
+18 |
a2d1dbebba | ||
|
|
605e744a58 | ||
|
|
aab0d6b18a | ||
|
|
dfde17111c | ||
|
|
2591ad12df | ||
|
|
3a05ae6f74 | ||
|
|
efcfd7bb9b | ||
|
|
bce62829b7 | ||
|
|
f320e486a2 | ||
|
|
8c96e04b51 | ||
|
|
9eb3698ea5 | ||
|
|
8ff07807f9 | ||
|
|
9c7f655329 | ||
|
|
5c72fca816 | ||
|
|
aeafa3ef0a | ||
|
|
3d4a3c7a93 | ||
|
|
1c1625d3fc | ||
|
|
75f9f045b8 | ||
|
|
86af383f6c | ||
|
|
84408025ed | ||
|
|
9fba8c548b | ||
|
|
56f3647ec2 | ||
|
|
a5cb3ce573 | ||
|
|
7581f4d641 | ||
|
|
a1d67b256e | ||
|
|
75f9dad16e | ||
|
|
0a0d212373 | ||
|
|
b8794ba807 | ||
|
|
9b8e4ed385 | ||
|
|
997b5d5290 | ||
|
|
d7080b41cb | ||
|
|
32754bd120 | ||
|
|
13fd02d174 | ||
|
|
d6cbac54e0 | ||
|
|
489ffdf49a | ||
|
|
37b22977ee | ||
|
|
848a040162 | ||
|
|
350d84a047 | ||
|
|
9c3cbee363 | ||
|
|
7e2ad3cea9 | ||
|
|
4ef52aa2c6 | ||
|
|
3132c64205 | ||
|
|
f25f8e0909 | ||
|
|
d8961f8eb9 | ||
|
|
9ff7cf650b | ||
|
|
ba4e717b59 | ||
|
|
f326a9a620 |
@@ -213,6 +213,10 @@ NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
# use organizations
|
||||
ORGANIZATIONS_ENABLED=
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
|
||||
# existing organizations based on email domain address
|
||||
ORGANIZATIONS_AUTOLINK=
|
||||
|
||||
# Vercel Config to create subdomains for organizations
|
||||
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
|
||||
PROJECT_ID_VERCEL=
|
||||
|
||||
@@ -7,7 +7,6 @@ public
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
*.test.ts
|
||||
|
||||
.gitignore
|
||||
.npmignore
|
||||
|
||||
@@ -526,7 +526,7 @@ For example, `Cal.com (support@cal.com)`.
|
||||
3. Copy Account SID to your `.env` file into the `TWILIO_SID` field
|
||||
4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field
|
||||
5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field
|
||||
6. Add your own sender id to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
|
||||
6. Add your own sender ID to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
|
||||
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||
8. Choose any name for the messaging service
|
||||
9. Click 'Add Senders'
|
||||
|
||||
@@ -70,6 +70,7 @@ const schemaEventTypeCreateParams = z
|
||||
recurringEvent: recurringEventInputSchema.optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
@@ -89,6 +90,7 @@ const schemaEventTypeEditParams = z
|
||||
length: z.number().int().optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
})
|
||||
@@ -129,6 +131,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
|
||||
@@ -94,6 +94,9 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* smsReminderNumber:
|
||||
* type: number
|
||||
* description: 'SMS reminder number'
|
||||
|
||||
@@ -146,6 +146,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
||||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* locations:
|
||||
* type: array
|
||||
* description: A list of all available locations for the event type
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
public/embed
|
||||
*.test.ts
|
||||
public/embed
|
||||
@@ -116,6 +116,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
type={item.integration.type}
|
||||
isChecked={cal.isSelected}
|
||||
destination={cal.externalId === props.destinationCalendarId}
|
||||
credentialId={cal.credentialId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -377,6 +377,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CheckboxField
|
||||
description={t("show_available_seats_count")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAvailabilityCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -53,6 +53,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
<ul className="p-4">
|
||||
{calendars?.map((calendar, i) => (
|
||||
<CalendarSwitch
|
||||
credentialId={calendar.credentialId}
|
||||
key={calendar.externalId}
|
||||
externalId={calendar.externalId}
|
||||
title={calendar.name || "Nameless Calendar"}
|
||||
|
||||
@@ -14,7 +14,6 @@ import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||
import { useFlags } from "@calcom/features/flags/hooks";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { MetaProvider } from "@calcom/ui";
|
||||
|
||||
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
|
||||
@@ -222,19 +221,7 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function useOrgBrandingValues() {
|
||||
const session = useSession();
|
||||
|
||||
const res = trpc.viewer.organizations.getBrand.useQuery(undefined, {
|
||||
// Only fetch if we have a session to avoid flooding logs with errors
|
||||
enabled: session.status === "authenticated",
|
||||
});
|
||||
|
||||
if (res.status === "loading") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (res.status === "error") return null;
|
||||
|
||||
return res.data;
|
||||
return session?.data?.user.org;
|
||||
}
|
||||
|
||||
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.9",
|
||||
"version": "3.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -5,10 +5,12 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
const selectedCalendarSelectSchema = z.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
credentialId: z.number().optional(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -24,7 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
timeZone: true,
|
||||
id: true,
|
||||
selectedCalendars: true,
|
||||
@@ -37,7 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { credentials, ...user } = userWithCredentials;
|
||||
|
||||
if (req.method === "POST") {
|
||||
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
|
||||
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
|
||||
await prisma.selectedCalendar.upsert({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
@@ -50,6 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
userId: user.id,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
},
|
||||
// already exists
|
||||
update: {},
|
||||
|
||||
@@ -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}
|
||||
@@ -917,6 +917,7 @@ const getEventTypesFromDB = async (id: number) => {
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
},
|
||||
@@ -940,6 +941,7 @@ const handleSeatsEventTypeOnBooking = async (
|
||||
eventType: {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
[x: string | number | symbol]: unknown;
|
||||
},
|
||||
bookingInfo: Partial<
|
||||
|
||||
@@ -113,6 +113,7 @@ export type FormValues = {
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
minimumBookingNotice: number;
|
||||
minimumBookingNoticeInDurationType: number;
|
||||
@@ -360,6 +361,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
@@ -426,6 +428,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
@@ -460,6 +463,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
@@ -516,6 +520,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
|
||||
@@ -715,14 +715,16 @@ const EventTypeListHeading = ({
|
||||
},
|
||||
});
|
||||
const bookerUrl = useBookerUrl();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Avatar
|
||||
alt={profile?.name || ""}
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
imageSrc={
|
||||
`${orgBranding?.fullDomain ?? WEBAPP_URL}${teamId ? "/team" : ""}/${profile.slug}/avatar.png` ||
|
||||
undefined
|
||||
orgBranding?.fullDomain
|
||||
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
|
||||
: profile.image
|
||||
}
|
||||
size="md"
|
||||
className="mt-1 inline-flex justify-center"
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function InsightsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ShellMain hideHeadingOnMobile>
|
||||
<ShellMain heading="Insights" subtitle={t("insights_subtitle")}>
|
||||
<UpgradeTip
|
||||
title={t("make_informed_decisions")}
|
||||
description={t("make_informed_decisions_description")}
|
||||
|
||||
@@ -182,6 +182,7 @@ const CalendarsView = () => {
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
credentialId={cal.credentialId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
name={cal.name || "Nameless calendar"}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -153,11 +153,9 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
) : (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||
<div className="text-muted dark:text-inverted p-8 text-center">
|
||||
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
|
||||
{" " + t("org_no_teams_yet")}
|
||||
</h2>
|
||||
<p className="mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
|
||||
<div className="text-muted p-8 text-center">
|
||||
<h2 className="font-cal text-emphasis mb-2 text-3xl">{" " + t("org_no_teams_yet")}</h2>
|
||||
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +210,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||
<EventTypes />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{(!team.hideBookATeamMember || team.isPrivate) && (
|
||||
{!team.hideBookATeamMember && !team.isPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
|
||||
@@ -1,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,
|
||||
},
|
||||
|
||||
@@ -135,6 +135,24 @@ test.describe("Manage Booking Questions", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Verify that we can prefill name field with no lastname", async () => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("name", "FirstName");
|
||||
await doOnFreshPreviewWithSearchParams(searchParams, page, context, async (page) => {
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await expectSystemFieldsToBeThereOnBookingPage({
|
||||
page,
|
||||
isFirstAndLastNameVariant: true,
|
||||
values: {
|
||||
name: {
|
||||
firstName: "FirstName",
|
||||
lastName: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -506,6 +524,27 @@ async function doOnFreshPreview(
|
||||
return previewTabPage;
|
||||
}
|
||||
|
||||
async function doOnFreshPreviewWithSearchParams(
|
||||
searchParams: URLSearchParams,
|
||||
page: Page,
|
||||
context: PlaywrightTestArgs["context"],
|
||||
callback: (page: Page) => Promise<void>,
|
||||
persistTab = false
|
||||
) {
|
||||
const previewUrl = (await page.locator('[data-testid="preview-button"]').getAttribute("href")) || "";
|
||||
const previewUrlObj = new URL(previewUrl);
|
||||
searchParams.forEach((value, key) => {
|
||||
previewUrlObj.searchParams.append(key, value);
|
||||
});
|
||||
const previewTabPage = await context.newPage();
|
||||
await previewTabPage.goto(previewUrlObj.toString());
|
||||
await callback(previewTabPage);
|
||||
if (!persistTab) {
|
||||
await previewTabPage.close();
|
||||
}
|
||||
return previewTabPage;
|
||||
}
|
||||
|
||||
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
||||
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="toggle-field"]').click();
|
||||
await saveEventType(page);
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"team_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Teamplan getestet haben. Wir haben festgestellt, dass Ihr Team „{{teamName}}“ aktualisiert werden muss.",
|
||||
"upgrade_banner_action": "Hier aufrüsten",
|
||||
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
|
||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
|
||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Tarif „Organization“ getestet haben. Wir haben festgestellt, dass Ihre Organization „{{teamName}}“ ein Upgrade benötigt.",
|
||||
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
|
||||
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
|
||||
"hey_there": "Hallo,",
|
||||
@@ -1681,7 +1681,7 @@
|
||||
"email_user_cta": "Einladung anzeigen",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
|
||||
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrem {{entity}} genießen.",
|
||||
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrer {{entity}} genießen.",
|
||||
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
|
||||
"email_no_user_step_two": "Verbinden Sie Ihr Kalenderkonto",
|
||||
"email_no_user_step_three": "Legen Sie Ihre Verfügbarkeit fest",
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"invite_team_individual_segment": "Invite individual",
|
||||
"invite_team_bulk_segment": "Bulk import",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"signin_or_signup_to_accept_invite": "You need to Sign in or Sign up to see team invitation.",
|
||||
"your_event_has_been_scheduled": "Your event has been scheduled",
|
||||
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
|
||||
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||
@@ -292,9 +291,9 @@
|
||||
"add_another_calendar": "Add another calendar",
|
||||
"other": "Other",
|
||||
"email_sign_in_subject": "Your sign-in link for {{appName}}",
|
||||
"emailed_you_and_attendees": "We emailed you and the other attendees a calendar invitation with all the details.",
|
||||
"emailed_you_and_attendees_recurring": "We emailed you and the other attendees a calendar invitation for the first of these recurring events.",
|
||||
"emailed_you_and_any_other_attendees": "You and any other attendees have been emailed with this information.",
|
||||
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
|
||||
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
|
||||
"user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.",
|
||||
@@ -592,6 +591,8 @@
|
||||
"invite_new_member": "Invite a new team member",
|
||||
"invite_new_member_description": "Note: This will <1>cost an extra seat ($15/m)</1> on your subscription.",
|
||||
"invite_new_team_member": "Invite someone to your team.",
|
||||
"upload_csv_file": "Upload a .csv file",
|
||||
"invite_via_email": "Invite via email",
|
||||
"change_member_role": "Change team member role",
|
||||
"disable_cal_branding": "Disable {{appName}} branding",
|
||||
"disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.",
|
||||
@@ -974,6 +975,8 @@
|
||||
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
|
||||
"seats_available_one": "Seat available",
|
||||
"seats_available_other": "Seats available",
|
||||
"seats_nearly_full": "Seats almost full",
|
||||
"seats_half_full": "Seats filling fast",
|
||||
"number_of_seats": "Number of seats per booking",
|
||||
"enter_number_of_seats": "Enter number of seats",
|
||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||
@@ -1100,7 +1103,7 @@
|
||||
"reschedule_optional": "Reason for rescheduling (optional)",
|
||||
"reschedule_placeholder": "Let others know why you need to reschedule",
|
||||
"event_cancelled": "This event is canceled",
|
||||
"emailed_information_about_cancelled_event": "We emailed you and the other attendees to let them know.",
|
||||
"emailed_information_about_cancelled_event": "We sent an email to everyone to let them know.",
|
||||
"this_input_will_shown_booking_this_event": "This input will be shown when booking this event",
|
||||
"meeting_url_in_confirmation_email": "Meeting url is in the confirmation email",
|
||||
"url_start_with_https": "URL needs to start with http:// or https://",
|
||||
@@ -1455,6 +1458,7 @@
|
||||
"add_limit": "Add Limit",
|
||||
"team_name_required": "Team name required",
|
||||
"show_attendees": "Share attendee information between guests",
|
||||
"show_available_seats_count": "Show the number of available seats",
|
||||
"how_booking_questions_as_variables": "How to use booking questions as variables?",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Use uppercase for all letters",
|
||||
@@ -2046,5 +2050,8 @@
|
||||
"no_members_found": "No members found",
|
||||
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
|
||||
"availability_schedules":"Availability Schedules",
|
||||
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
|
||||
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
|
||||
"edit_users_availability":"Edit user's availability: {{username}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1888,7 +1888,7 @@
|
||||
"organization_name": "Nombre de la organización",
|
||||
"organization_url": "URL de la organización",
|
||||
"organization_verify_header": "Verifique el correo electrónico de su organización",
|
||||
"organization_verify_email_body": "Utilice el código a continuación para verificar su dirección de correo electrónico para seguir configurando su organización.",
|
||||
"organization_verify_email_body": "Utilice el siguiente código para verificar su dirección de correo electrónico y continuar con la configuración de su organización.",
|
||||
"additional_url_parameters": "Parámetros adicionales de URL",
|
||||
"about_your_organization": "Acerca de su organización",
|
||||
"about_your_organization_description": "Las organizaciones son entornos compartidos donde puede crear varios equipos con miembros, tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.",
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"invite_team_individual_segment": "Inviter une personne",
|
||||
"invite_team_bulk_segment": "Importation multiple",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"signin_or_signup_to_accept_invite": "Vous devez vous connecter ou vous inscrire pour voir l'invitation d'équipe.",
|
||||
"your_event_has_been_scheduled": "Votre événement a été planifié",
|
||||
"your_event_has_been_scheduled_recurring": "Votre événement récurrent a été planifié",
|
||||
"accept_our_license": "Acceptez notre licence en changeant la variable .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> en « {{agree}} ».",
|
||||
@@ -292,9 +291,9 @@
|
||||
"add_another_calendar": "Ajouter un autre calendrier",
|
||||
"other": "Autre",
|
||||
"email_sign_in_subject": "Votre lien de connexion pour {{appName}}",
|
||||
"emailed_you_and_attendees": "Vous et tous les autres participants avez reçu ces informations par e-mail.",
|
||||
"emailed_you_and_attendees_recurring": "Vous et tous les autres participants avez reçu par e-mail les informations pour le premier de ces événements récurrents.",
|
||||
"emailed_you_and_any_other_attendees": "Vous et tous les autres participants avez reçu ces informations par e-mail.",
|
||||
"emailed_you_and_attendees": "Nous avons envoyé un e-mail avec une invitation de calendrier comprenant les détails à tout le monde.",
|
||||
"emailed_you_and_attendees_recurring": "Nous avons envoyé un e-mail avec une invitation de calendrier comprenant les détails à tout le monde pour le premier de ces événements récurrents.",
|
||||
"emailed_you_and_any_other_attendees": "Nous avons envoyé cette information à tout le monde par e-mail.",
|
||||
"needs_to_be_confirmed_or_rejected": "Votre réservation doit encore être confirmée ou refusée.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Votre rendez-vous récurrent doit encore être confirmé ou refusé.",
|
||||
"user_needs_to_confirm_or_reject_booking": "{{user}} doit encore confirmer ou refuser la réservation.",
|
||||
@@ -592,6 +591,8 @@
|
||||
"invite_new_member": "Inviter un nouveau membre d'équipe",
|
||||
"invite_new_member_description": "Remarque : cela <1>coûtera une place supplémentaire (15 $/mois)</1> sur votre abonnement.",
|
||||
"invite_new_team_member": "Invitez quelqu'un à rejoindre votre équipe.",
|
||||
"upload_csv_file": "Télécharger un fichier .csv",
|
||||
"invite_via_email": "Inviter par e-mail",
|
||||
"change_member_role": "Changer le rôle du membre de l'équipe",
|
||||
"disable_cal_branding": "Désactiver les références à la marque {{appName}}",
|
||||
"disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}} de vos pages publiques.",
|
||||
@@ -974,6 +975,8 @@
|
||||
"offer_seats_description": "Proposez des places de réservation. Cela désactive automatiquement les réservations d'invités et d'opt-in.",
|
||||
"seats_available_one": "Place disponible",
|
||||
"seats_available_other": "Places disponibles",
|
||||
"seats_nearly_full": "Places presque toutes occupées",
|
||||
"seats_half_full": "Les places partent vite",
|
||||
"number_of_seats": "Nombre de places par réservation",
|
||||
"enter_number_of_seats": "Saisir le nombre de sièges",
|
||||
"you_can_manage_your_schedules": "Vous pouvez gérer vos disponibilités sur la page Disponibilité.",
|
||||
@@ -1100,7 +1103,7 @@
|
||||
"reschedule_optional": "Raison de la replanification (facultatif)",
|
||||
"reschedule_placeholder": "Faites savoir aux autres pourquoi vous devez replanifier le rendez-vous.",
|
||||
"event_cancelled": "Cet événement est annulé",
|
||||
"emailed_information_about_cancelled_event": "Nous vous avons envoyé un e-mail à vous et aux autres participants pour les en informer.",
|
||||
"emailed_information_about_cancelled_event": "Nous avons envoyé un e-mail à tout le monde pour les en informer.",
|
||||
"this_input_will_shown_booking_this_event": "Ce champ sera affiché lors de la réservation de cet événement.",
|
||||
"meeting_url_in_confirmation_email": "Le lien du rendez-vous est dans l'e-mail de confirmation",
|
||||
"url_start_with_https": "Le lien doit commencer par http:// ou https://",
|
||||
@@ -1455,6 +1458,7 @@
|
||||
"add_limit": "Ajouter une limite",
|
||||
"team_name_required": "Nom d'équipe requis",
|
||||
"show_attendees": "Partagez les informations des participants entre les invités",
|
||||
"show_available_seats_count": "Afficher le nombre de places disponibles",
|
||||
"how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
||||
|
||||
@@ -210,6 +210,8 @@
|
||||
"done": "Učinjeno",
|
||||
"all_done": "Završeno!",
|
||||
"all": "Sve",
|
||||
"available_apps": "Dostupne Aplikacije",
|
||||
"available_apps_lower_case": "Dostupne aplikacije",
|
||||
"check_email_reset_password": "Provjerite svoju e-poštu. Poslali smo vam link za resetiranje lozinke.",
|
||||
"finish": "Završi",
|
||||
"few_sentences_about_yourself": "Nekoliko rečenica o sebi. Ovo će se pojaviti na vašoj osobnoj stranici.",
|
||||
@@ -331,5 +333,6 @@
|
||||
"dark": "Tamna",
|
||||
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
|
||||
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
|
||||
"full_name": "Puno ime",
|
||||
"insights_all_org_filter": "Sve"
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
"layout": "Raspored",
|
||||
"bookerlayout_default_title": "Podrazumevani prikaz",
|
||||
"bookerlayout_description": "Možete da izaberete više njih, a vaši učesnici mogu da menjaju prikaze.",
|
||||
"bookerlayout_user_settings_title": "Raspored zakazivanja",
|
||||
"bookerlayout_user_settings_title": "Režim prikaza rezervacija",
|
||||
"bookerlayout_user_settings_description": "Možete da izaberete više njih, a učesnici mogu da menjaju prikaz. Ovo se može zameniti za svaki događaj.",
|
||||
"bookerlayout_month_view": "Mesec",
|
||||
"bookerlayout_week_view": "Nedeljno",
|
||||
@@ -404,7 +404,7 @@
|
||||
"recording_ready": "Link za preuzimanje snimka je spreman",
|
||||
"booking_created": "Rezervacija Napravljena",
|
||||
"booking_rejected": "Rezevacija je odbijena",
|
||||
"booking_requested": "Rezervacija je zahtevana",
|
||||
"booking_requested": "Zahtev za rezervaciju je poslat",
|
||||
"meeting_ended": "Sastanak se završio",
|
||||
"form_submitted": "Formular poslat",
|
||||
"event_triggers": "Okidači Dogadjaja",
|
||||
@@ -552,11 +552,11 @@
|
||||
"team_description": "Par rečenica o vašem timu. Ovo će se pojaviti na stranici URL adrese vašeg tima.",
|
||||
"org_description": "Nekoliko rečenica o vašoj organizaciji. Ovo će se pojaviti na url stranici vaše organizacije.",
|
||||
"members": "Članovi",
|
||||
"organization_members": "Članovi organizacije",
|
||||
"organization_members": "Korisnici uključeni u tarifni plan Organization",
|
||||
"member": "Član",
|
||||
"number_member_one": "{{count}} član",
|
||||
"number_member_other": "{{count}} članova",
|
||||
"number_selected": "{{count}} izabrano",
|
||||
"number_selected": "Izabrano: {{count}}",
|
||||
"owner": "Vlasnik",
|
||||
"admin": "Admin",
|
||||
"administrator_user": "Administrator",
|
||||
@@ -1687,7 +1687,7 @@
|
||||
"attendee_no_longer_attending": "Polaznik više ne pohađa vaš događaj",
|
||||
"attendee_no_longer_attending_subtitle": "Korisnik {{name}} je otkazao. To znači da se otvorilo mesto za ovaj vremenski period",
|
||||
"create_event_on": "Kreirajte događaj na",
|
||||
"create_routing_form_on": "Kreiraj obrazac za usmeravanje",
|
||||
"create_routing_form_on": "Kreiranje obrasca za usmeravanje za",
|
||||
"default_app_link_title": "Podesite podrazumevani link aplikacije",
|
||||
"default_app_link_description": "Podešavanje podrazumevanog linka aplikacije omogućava novokreiranim tipovima događaja da koriste link aplikacije koji ste postavili.",
|
||||
"organizer_default_conferencing_app": "Podrazumevana aplikacija organizatora",
|
||||
@@ -1876,23 +1876,23 @@
|
||||
"connect_google_workspace": "Poveži Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Morate da budete Workspace administrator da biste koristili ovu opciju",
|
||||
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
|
||||
"install_app_on": "Instaliraj aplikaciju",
|
||||
"install_app_on": "Instaliraj aplikaciju za",
|
||||
"create_for": "Napravi za",
|
||||
"organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
|
||||
"organization_banner_description": "Kreirajte okruženja gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
|
||||
"organization_banner_title": "Upravljajte organizacijama sa više timova",
|
||||
"set_up_your_organization": "Postavite svoju organizaciju",
|
||||
"set_up_your_organization": "Konfigurisanje profila organizacije",
|
||||
"organizations_description": "Organizacije su deljena okruženja gde timovi mogu da kreiraju deljene vrste događaja, aplikacije, radne tokove i još mnogo toga.",
|
||||
"must_enter_organization_name": "Morate da unesete naziv organizacije",
|
||||
"must_enter_organization_admin_email": "Morate da unesete adresu e-pošte vaše organizacije",
|
||||
"admin_email": "Imejl adresa vaše organizacije",
|
||||
"admin_email": "Vaša adresa e-pošte u organizaciji",
|
||||
"admin_username": "Korisničko ime administratora",
|
||||
"organization_name": "Naziv organizacije",
|
||||
"organization_url": "URL organizacije",
|
||||
"organization_verify_header": "Potvrdite imejl vaše organizacije",
|
||||
"organization_verify_header": "Potvrdite svoju adresu e-pošte u organizaciji",
|
||||
"organization_verify_email_body": "Koristite kôd u nastavku da potvrdite svoju adresu e-pošte kako biste nastavili sa podešavanjem svoje organizacije.",
|
||||
"additional_url_parameters": "Dodatni URL parametri",
|
||||
"about_your_organization": "O vašoj organizaciji",
|
||||
"about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrste događaja, aplikacije, radne tokove i drugo.",
|
||||
"about_your_organization_description": "Organizacije su deljena okruženja gde možete da kreirate više timova sa deljenim članovima, vrstama događaja, aplikacijama, radnim tokovima i mnogim drugim stvarima.",
|
||||
"create_your_teams": "Kreirajte svoje timove",
|
||||
"create_your_teams_description": "Počnite planiranje zajedno dodavanjem članova tima u svoju organizaciju",
|
||||
"invite_organization_admins": "Pozovite administratore vaše organizacije",
|
||||
@@ -1917,7 +1917,7 @@
|
||||
"org_no_teams_yet_description": "Ako ste administrator, obavezno kreirajte timove koji će ovde biti prikazani.",
|
||||
"set_up": "Podesi",
|
||||
"set_up_your_profile": "Podesite svoj profil",
|
||||
"set_up_your_profile_description": "Neka ljudi u organizaciji {{orgName}} znaju ko ste i kako da se povežu sa vama putem javnog linka.",
|
||||
"set_up_your_profile_description": "Unesite informacije o svojoj ulozi u organizaciji {{orgName}}. Te informacije će videti osobe koje kliknu na vaš javni link.",
|
||||
"my_profile": "Moj profil",
|
||||
"my_settings": "Moja podešavanja",
|
||||
"crm": "CRM",
|
||||
@@ -1925,7 +1925,7 @@
|
||||
"sender_id_info": "Ime ili broj koji će biti prikazani kao pošiljalac SMS-a (neke zemlje ne dozvoljavaju alfanumeričke ID-ove pošiljaoca)",
|
||||
"org_admins_can_create_new_teams": "Samo administrator vaše ogranizacije može da kreira nove timove",
|
||||
"google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.",
|
||||
"resolve": "Reši",
|
||||
"resolve": "Kako rešiti taj problem",
|
||||
"no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.",
|
||||
"org_name": "Naziv organizacije",
|
||||
"org_url": "URL organizacije",
|
||||
@@ -1933,7 +1933,7 @@
|
||||
"404_the_org": "Organizacija",
|
||||
"404_the_team": "Tim",
|
||||
"404_claim_entity_org": "Zatražite poddomen za svoju organizaciju",
|
||||
"404_claim_entity_team": "Zahtevajte ovaj tim i počnite da uređujete kolektivni raspored",
|
||||
"404_claim_entity_team": "Postanite deo ovog tima i počnite da uređujete kolektivni raspored",
|
||||
"insights_all_org_filter": "Sve aplikacije",
|
||||
"insights_team_filter": "Tim: {{teamName}}",
|
||||
"insights_user_filter": "Korisnik: {{userName}}",
|
||||
|
||||
@@ -17,9 +17,15 @@
|
||||
"verify_email_banner_body": "Verifiera din e-postadress för att garantera bästa e-post- och kalenderleverans",
|
||||
"verify_email_email_header": "Verifiera din e-postadress",
|
||||
"verify_email_email_button": "Verifiera e-post",
|
||||
"copy_somewhere_safe": "Spara denna API-nyckel någonstans säkert. Du kommer inte att kunna visa den igen.",
|
||||
"verify_email_email_body": "Verifiera din e-postadress genom att klicka på knappen nedan.",
|
||||
"verify_email_by_code_email_body": "Verifiera din e-postadress genom att använda nedanstående kod.",
|
||||
"verify_email_email_link_text": "Här är länken om du inte gillar att klicka på knappar:",
|
||||
"email_verification_code": "Ange verifieringskod",
|
||||
"email_verification_code_placeholder": "Ange verifieringskoden som skickats till din e-post",
|
||||
"incorrect_email_verification_code": "Vertifieringskoden är felaktig.",
|
||||
"email_sent": "E-postmeddelandet har skickats",
|
||||
"email_not_sent": "Ett fel uppstod när e-post skulle skickas",
|
||||
"event_declined_subject": "Avvisades: {{title}} kl. {{date}}",
|
||||
"event_cancelled_subject": "Avbokad: {{title}} {{date}}",
|
||||
"event_request_declined": "Din bokningsförfrågan har avbjöts",
|
||||
@@ -78,6 +84,7 @@
|
||||
"event_awaiting_approval_recurring": "En återkommande händelse väntar på ditt godkännande",
|
||||
"someone_requested_an_event": "Någon har begärt att schemalägga en händelse i din kalender.",
|
||||
"someone_requested_password_reset": "Någon har begärt en länk för att ändra ditt lösenord.",
|
||||
"password_reset_email_sent": "Om denna e-postadress finns i vårt system bör du få ett återställningsmeddelande.",
|
||||
"password_reset_instructions": "Om du inte begärde detta kan du ignorera detta e-postmeddelande och ditt lösenord kommer inte att ändras.",
|
||||
"event_awaiting_approval_subject": "Inväntar godkännande: {{title}} den {{date}}",
|
||||
"event_still_awaiting_approval": "En bokning väntar fortfarande på ditt godkännande",
|
||||
@@ -217,6 +224,10 @@
|
||||
"already_have_an_account": "Har du redan ett konto?",
|
||||
"create_account": "Skapa konto",
|
||||
"confirm_password": "Bekräfta lösenord",
|
||||
"confirm_auth_change": "Detta kommer att ändra hur du loggar in",
|
||||
"confirm_auth_email_change": "Att ändra e-postadressen kommer att koppla bort din nuvarande autentiseringsmetod för att logga in på Cal.com. Vi kommer att be dig verifiera din nya e-postadress. Framå kommer du att loggas ut och använda din nya e-postadress för att logga in istället för din nuvarande autentiseringsmetod efter att du ställt in ditt lösenord genom att följa instruktionerna som kommer att skickas till din e-post.",
|
||||
"reset_your_password": "Konfigurera ditt nya lösenord med instruktionerna som skickas till din e-postadress.",
|
||||
"email_change": "Logga in igen med din nya e-postadress och lösenord.",
|
||||
"create_your_account": "Skapa ditt konto",
|
||||
"sign_up": "Registrera dig",
|
||||
"youve_been_logged_out": "Du har loggats ut",
|
||||
@@ -243,6 +254,10 @@
|
||||
"all": "Alla",
|
||||
"yours": "Ditt konto",
|
||||
"available_apps": "Tillgängliga appar",
|
||||
"available_apps_lower_case": "Tillgängliga appar",
|
||||
"available_apps_desc": "Du har inga appar installerade. Visa populära appar nedan och utforska mer i vår <1>App Store</1>",
|
||||
"fixed_host_helper": "Lägg till alla som behöver delta i evenemanget. <1>Läs mer</1>",
|
||||
"round_robin_helper": "Personer i gruppen turas om och endast en person kommer att dyka upp för evenemanget.",
|
||||
"check_email_reset_password": "Kolla din e-post. Vi har skickat en länk till dig för att återställa ditt lösenord.",
|
||||
"finish": "Slutför",
|
||||
"organization_general_description": "Hantera inställningar för ditt teams språk och tidszon",
|
||||
@@ -251,6 +266,7 @@
|
||||
"nearly_there_instructions": "Till sist så hjälper en kort beskrivning om dig själv och ett foto dig verkligen att få bokningar och andra kan veta vem de bokar med.",
|
||||
"set_availability_instructions": "Definiera tidsintervall när du är tillgänglig på en återkommande basis. Du kan skapa fler av dessa senare och tilldela dem till olika kalendrar.",
|
||||
"set_availability": "Ställ in din tillgänglighet",
|
||||
"availability_settings": "Tillgänglighetsinställningar",
|
||||
"continue_without_calendar": "Fortsätt utan kalender",
|
||||
"connect_your_calendar": "Anslut din kalender",
|
||||
"connect_your_video_app": "Anslut dina videoappar",
|
||||
@@ -392,6 +408,8 @@
|
||||
"allow_dynamic_booking_tooltip": "Gruppbokningslänkar som kan skapas dynamiskt genom att lägga till flera användarnamn med ett '+'. Exempel: '{{appName}}/bailey+peer'",
|
||||
"allow_dynamic_booking": "Tillåt deltagare att boka dig via dynamiska gruppbokningar",
|
||||
"dynamic_booking": "Dynamiska grupplänkar",
|
||||
"allow_seo_indexing": "Tillåt sökmotorer att komma åt ditt publika innehåll",
|
||||
"seo_indexing": "Tillåt SEO-indexering",
|
||||
"email": "E-post",
|
||||
"email_placeholder": "jdoe@exempel.se",
|
||||
"full_name": "Fullständigt namn",
|
||||
@@ -406,6 +424,7 @@
|
||||
"booking_requested": "Bokning begärd",
|
||||
"meeting_ended": "Mötet avslutat",
|
||||
"form_submitted": "Formulär skickat",
|
||||
"booking_paid": "Bokning betald",
|
||||
"event_triggers": "Händelseutlösare",
|
||||
"subscriber_url": "Prenumerantens URL",
|
||||
"create_new_webhook": "Skapa en ny webhook",
|
||||
@@ -506,6 +525,7 @@
|
||||
"your_name": "Ditt namn",
|
||||
"your_full_name": "Ditt fullständiga namn",
|
||||
"no_name": "Inget namn",
|
||||
"enter_number_between_range": "Ange ett tal mellan 1 och {{maxOccurences}}",
|
||||
"email_address": "E-postadress",
|
||||
"enter_valid_email": "Ange en giltig e-postadress",
|
||||
"location": "Plats",
|
||||
@@ -543,6 +563,7 @@
|
||||
"leave": "Lämna",
|
||||
"profile": "Profil",
|
||||
"my_team_url": "Min team-URL",
|
||||
"my_teams": "Mina team",
|
||||
"team_name": "Teamnamn",
|
||||
"your_team_name": "Ditt team-namn",
|
||||
"team_updated_successfully": "Teamet har uppdaterats",
|
||||
@@ -570,6 +591,8 @@
|
||||
"invite_new_member": "Bjud in en ny teammedlem",
|
||||
"invite_new_member_description": "Obs! Detta kommer att <1>kosta en extra plats (150 kr/mån)</1> i din prenumeration.",
|
||||
"invite_new_team_member": "Bjud in någon till ditt team.",
|
||||
"upload_csv_file": "Ladda upp en .csv-fil",
|
||||
"invite_via_email": "Bjud in via e-post",
|
||||
"change_member_role": "Ändra teammedlemmens roll",
|
||||
"disable_cal_branding": "Inaktivera {{appName}}-varumärke",
|
||||
"disable_cal_branding_description": "Dölj all {{appName}} reklam från dina offentliga sidor.",
|
||||
@@ -842,6 +865,7 @@
|
||||
"team_view_user_availability_disabled": "Användaren måste acceptera inbjudan för att visa tillgängligheten",
|
||||
"set_as_away": "Ställ in dig själv som inte tillgänglig",
|
||||
"set_as_free": "Ställ av din status som inte tillgänglig",
|
||||
"toggle_away_error": "Fel vid uppdatering av frånvarostatus",
|
||||
"user_away": "Den här användaren är för inte tillgänglig.",
|
||||
"user_away_description": "Personen du försöker boka har satt sig som inte tillgänglig och accepterar därför inte nya bokningar.",
|
||||
"meet_people_with_the_same_tokens": "Träffa personer med samma tokens",
|
||||
@@ -851,6 +875,7 @@
|
||||
"account_managed_by_identity_provider_description": "Besök dina {{provider}} -kontoinställningar för att ändra din e-post, lösenord, aktivera tvåfaktorsautentisering mm.",
|
||||
"signin_with_google": "Logga in med Google",
|
||||
"signin_with_saml": "Logga in med SAML",
|
||||
"signin_with_saml_oidc": "Logga in med SAML/OIDC",
|
||||
"you_will_need_to_generate": "Du måste generera en åtkomsttoken från ditt gamla schemaläggningsverktyg.",
|
||||
"import": "Importera",
|
||||
"import_from": "Importera från",
|
||||
@@ -950,6 +975,8 @@
|
||||
"offer_seats_description": "Erbjud platser till bokningar. Detta inaktiverar gäster och bokningar.",
|
||||
"seats_available_one": "Tillgänglig plats",
|
||||
"seats_available_other": "Tillgängliga platser",
|
||||
"seats_nearly_full": "Sittplatser nästan fulla",
|
||||
"seats_half_full": "Sittplatser fylls snabbt",
|
||||
"number_of_seats": "Antal platser per bokning",
|
||||
"enter_number_of_seats": "Ange antal platser",
|
||||
"you_can_manage_your_schedules": "Du kan hantera dina scheman på sidan Tillgänglighet.",
|
||||
@@ -1030,12 +1057,15 @@
|
||||
"how_you_want_add_cal_site": "Hur vill du lägga till {{appName}} till din webbplats?",
|
||||
"choose_ways_put_cal_site": "Välj ett av följande sätt att lägga {{appName}} på din webbplats.",
|
||||
"setting_up_zapier": "Ställa in din Zapier-integration",
|
||||
"setting_up_make": "Konfigurera din Make-integration",
|
||||
"generate_api_key": "Generera API-nyckel",
|
||||
"generate_api_key_description": "Skapa en API-nyckel att använda med {{appName}} på",
|
||||
"your_unique_api_key": "Din unika API-nyckel",
|
||||
"copy_safe_api_key": "Kopiera denna API-nyckel och spara den någonstans säkert. Om du förlorar denna nyckel måste du skapa en ny.",
|
||||
"zapier_setup_instructions": "<0>Logga in på ditt Zapier-konto och skapa en ny Zap.</0><1>Välj {{appName}} som din Trigger-app. Välj också en Trigger-händelse. /1><2>Välj ditt konto och ange sedan din unika API-nyckel.</2><3>Testa din Trigger.</3><4>Du är klar!</4>",
|
||||
"make_setup_instructions": "<0>Gå till <1><0>Skapa inbjudningslänk</0></1> och installera Cal.com-appen.</0><1>Logga in på ditt Make-konto och skapa ett nytt Scenario.</1><2>Välj Cal.com som din Trigger-app. Välj också en triggerhändelse.</2><3>Välj ditt konto och ange sedan din unika API-nyckel.</3><4>Testa din trigger.</4><5>Du är redo!</5>",
|
||||
"install_zapier_app": "Installera först Zapier-appen i appbutiken.",
|
||||
"install_make_app": "Installera först Make-appen i appbutiken.",
|
||||
"connect_apple_server": "Anslut till Apple Server",
|
||||
"calendar_url": "Kalender-URL",
|
||||
"apple_server_generate_password": "Skapa ett appspecifikt lösenord som du kan använda med {{appName}} på",
|
||||
@@ -1085,6 +1115,7 @@
|
||||
"email_attendee_action": "skicka e-post till deltagare",
|
||||
"sms_attendee_action": "Skicka SMS till deltagare",
|
||||
"sms_number_action": "skicka SMS till ett specifikt nummer",
|
||||
"send_reminder_sms": "Skicka enkelt mötespåminnelser via SMS till dina deltagare",
|
||||
"whatsapp_number_action": "skicka Whatsapp till ett specifikt nummer",
|
||||
"whatsapp_attendee_action": "skicka Whatsapp till deltagare",
|
||||
"workflows": "Arbetsflöden",
|
||||
@@ -1244,6 +1275,7 @@
|
||||
"error_updating_settings": "Fel vid uppdatering av inställningar",
|
||||
"personal_cal_url": "Min personliga {{appName}}-URL",
|
||||
"bio_hint": "Några meningar om dig själv. Detta kommer att visas på din personliga webbsida.",
|
||||
"user_has_no_bio": "Den här användaren har inte lagt till en biografi ännu.",
|
||||
"delete_account_modal_title": "Radera konto",
|
||||
"confirm_delete_account_modal": "Är du säker på att du vill radera ditt {{appName}}-konto?",
|
||||
"delete_my_account": "Radera mitt konto",
|
||||
@@ -1426,6 +1458,7 @@
|
||||
"add_limit": "Lägg till gräns",
|
||||
"team_name_required": "Teamnamn krävs",
|
||||
"show_attendees": "Dela deltagarinformation mellan gäster",
|
||||
"show_available_seats_count": "Visa antalet lediga platser",
|
||||
"how_booking_questions_as_variables": "Hur används bokningsfrågor som variabler?",
|
||||
"format": "Formatera",
|
||||
"uppercase_for_letters": "Använd versaler för alla bokstäver",
|
||||
@@ -1659,8 +1692,11 @@
|
||||
"delete_sso_configuration_confirmation_description": "Vill du verkligen ta bort {{connectionType}}-konfigurationen? Dina teammedlemmar som använder {{connectionType}}-inloggning kan inte längre komma åt Cal.com.",
|
||||
"organizer_timezone": "Organisatörens tidszon",
|
||||
"email_user_cta": "Visa inbjudan",
|
||||
"email_no_user_invite_heading_team": "Du har blivit inbjuden att gå med i ett {{appName}}-team",
|
||||
"email_no_user_invite_heading_org": "Du har blivit inbjuden att gå med i en {{appName}}-organisation",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i sitt team på {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} har bjudit in dig att gå med i sitt team {{teamName}} den {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} har bjudit in dig att gå med i deras organisation \"{{teamName}}\" på {{appName}}. {{appName}} är schemaläggaren som gör det möjligt för dig och din organisation att schemalägga möten utan e-posttennis.",
|
||||
"email_no_user_invite_steps_intro": "Vi guidar dig genom några korta steg och du kommer att kunna schemalägga med din/ditt {{entity}} på nolltid utan stress.",
|
||||
"email_no_user_step_one": "Välj ditt användarnamn",
|
||||
"email_no_user_step_two": "Anslut ditt kalenderkonto",
|
||||
@@ -1831,6 +1867,8 @@
|
||||
"insights_no_data_found_for_filter": "Inga data hittades för valt filter eller valda datum.",
|
||||
"acknowledge_booking_no_show_fee": "Jag bekräftar att en avgift på {{amount, currency}} för utebliven närvaro debiteras mitt kort om jag inte deltar i händelsen.",
|
||||
"card_details": "Kortuppgifter",
|
||||
"something_went_wrong_on_our_end": "Något gick fel från vår sida. Kontakta vårt supportteam så fixar vi det åt dig direkt.",
|
||||
"please_provide_following_text_to_suppport": "Ange följande text när du kontaktar supporten så att vi kan hjälpa dig bättre",
|
||||
"seats_and_no_show_fee_error": "För närvarande går det inte att aktivera platser och ta ut en avgift för utebliven närvaro",
|
||||
"complete_your_booking": "Slutför din bokning",
|
||||
"complete_your_booking_subject": "Slutför din bokning: {{title}} {{date}}",
|
||||
@@ -1864,6 +1902,9 @@
|
||||
"open_dialog_with_element_click": "Öppna din Cal-dialogruta när någon klickar på ett element.",
|
||||
"need_help_embedding": "Behöver du hjälp? Se våra guider för inbäddning av Cal på Wix, Squarespace eller WordPress, kolla in våra vanliga frågor eller utforska avancerade inbäddningsalternativ.",
|
||||
"book_my_cal": "Boka i min Cal",
|
||||
"first_name": "Förnamn",
|
||||
"last_name": "Efternamn",
|
||||
"first_last_name": "Förnamn, Efternamn",
|
||||
"invite_as": "Bjud in som",
|
||||
"form_updated_successfully": "Formuläret har uppdaterats.",
|
||||
"disable_attendees_confirmation_emails": "Inaktivera förinställda e-postbekräftelser för deltagare",
|
||||
@@ -1877,6 +1918,7 @@
|
||||
"first_event_type_webhook_description": "Skapa din första webhook för denna händelsetyp",
|
||||
"install_app_on": "Installera appen på",
|
||||
"create_for": "Skapa för",
|
||||
"currency": "Valuta",
|
||||
"organization_banner_description": "Skapa miljöer där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
|
||||
"organization_banner_title": "Hantera organisationer med flera team",
|
||||
"set_up_your_organization": "Konfigurera din organisation",
|
||||
@@ -1937,6 +1979,79 @@
|
||||
"insights_team_filter": "Team: {{teamName}}",
|
||||
"insights_user_filter": "Användare: {{userName}}",
|
||||
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
|
||||
"location_options": "{{locationCount}} platsalternativ",
|
||||
"custom_plan": "Anpassad plan",
|
||||
"email_embed": "E-post inbäddad",
|
||||
"add_times_to_your_email": "Välj några tillgängliga tider och bädda in dem i din e-post",
|
||||
"select_time": "Välj tid",
|
||||
"select_date": "Välj datum",
|
||||
"see_all_available_times": "Se alla tillgängliga tider",
|
||||
"org_team_names_example": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_1": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_2": "t.ex. säljteam",
|
||||
"org_team_names_example_3": "t.ex. designteam",
|
||||
"org_team_names_example_4": "t.ex. ingenjörsteam",
|
||||
"org_team_names_example_5": "t.ex. dataanalytikerteam",
|
||||
"org_max_team_warnings": "Du kommer att kunna lägga till fler team senare.",
|
||||
"what_is_this_meeting_about": "Vad handlar det här mötet om?",
|
||||
"add_to_team": "Lägg till i teamet",
|
||||
"remove_users_from_org": "Ta bort användare från organisationen",
|
||||
"remove_users_from_org_confirm": "Är du säker på att du vill ta bort {{userCount}} användare från denna organisation?",
|
||||
"user_has_no_schedules": "Den här användaren har inte skapat några scheman ännu",
|
||||
"user_isnt_in_any_teams": "Den här användaren är inte med i något team",
|
||||
"requires_booker_email_verification": "Kräver e-postverifiering för bokare",
|
||||
"description_requires_booker_email_verification": "För att säkerställa bokarens e-postverifiering före schemaläggning av evenemang",
|
||||
"requires_confirmation_mandatory": "Textmeddelanden kan endast skickas till deltagare när evenemangstypen kräver bekräftelse.",
|
||||
"kyc_verification_information": "För att garantera säkerheten måste du verifiera ditt {{teamOrAccount}} innan du skickar textmeddelanden till deltagare. Kontakta oss på <a>{{supportEmail}}</a> och ange följande information:",
|
||||
"kyc_verification_documents": "<ul><li>Din {{teamOrUser}}</li><li>För företag: Bifoga din dokumentation för verifiering av företag</li><li>För privatpersoner: Bifoga ett statligt utfärdat ID-kort</li></ul>",
|
||||
"verify_team_or_account": "Verifiera {{teamOrAccount}}",
|
||||
"verify_account": "Verifiera konto",
|
||||
"kyc_verification": "KYC-verifiering",
|
||||
"organizations": "Organisationer",
|
||||
"org_admin_other_teams": "Andra team",
|
||||
"org_admin_other_teams_description": "Här kan du se team i din organisation som du inte är en del av. Du kan lägga till dem om det behövs.",
|
||||
"no_other_teams_found": "Inga andra team hittades",
|
||||
"no_other_teams_found_description": "Det finns inga andra team i denna organisation.",
|
||||
"attendee_first_name_variable": "Deltagarens förnamn",
|
||||
"attendee_last_name_variable": "Deltagarens efternamn",
|
||||
"attendee_first_name_info": "Förnamnet på personen som bokat",
|
||||
"attendee_last_name_info": "Efternamnet på personen som bokat",
|
||||
"me": "Jag",
|
||||
"verify_team_tooltip": "Verifiera ditt team för att aktivera att skicka meddelanden till deltagare",
|
||||
"member_removed": "Medlem borttagen",
|
||||
"my_availability": "Min tillgänglighet",
|
||||
"team_availability": "Teamets tillgänglighet",
|
||||
"backup_code": "Säkerhetskopiera kod",
|
||||
"backup_codes": "Säkerhetskopiera koder",
|
||||
"backup_code_instructions": "Varje säkerhetskopierad kod kan användas exakt en gång för att ge åtkomst utan din autentiserare.",
|
||||
"backup_codes_copied": "Säkerhetskopierade koder kopierade!",
|
||||
"incorrect_backup_code": "Den säkerhetskopierade koden är felaktig.",
|
||||
"lost_access": "Förlorad åtkomst",
|
||||
"missing_backup_codes": "Inga säkerhetskopieringskoder hittades. Generera dem i dina inställningar.",
|
||||
"admin_org_notification_email_subject": "Ny organisation skapad: väntande åtgärd",
|
||||
"hi_admin": "Hej administratör",
|
||||
"admin_org_notification_email_title": "En organisation kräver DNS-inställning",
|
||||
"admin_org_notification_email_body_part1": "En organisation med slug \"{{orgSlug}}\" skapades.<br /><br />Se till att konfigurera ditt DNS-register så att underdomänen motsvarar den nya organisationen där huvudappen körs. Annars kommer organisationen inte att fungera.<br /><br />Här är bara de allra grundläggande alternativen för att konfigurera en underdomän för att peka på deras app så den laddar organisationens profilsida.<br /><br />Du kan göra det antingen med A-posten:",
|
||||
"admin_org_notification_email_body_part2": "Eller CNAME-posten:",
|
||||
"admin_org_notification_email_body_part3": "När du har konfigurerat underdomänen, markera DNS-konfigurationen som gjort i organisationens admin-inställningar.",
|
||||
"admin_org_notification_email_cta": "Gå till organisationens admin-inställningar",
|
||||
"org_has_been_processed": "Org har behandlats",
|
||||
"org_error_processing": "Det har uppstått ett fel vid behandlingen av denna organisation",
|
||||
"orgs_page_description": "En lista över alla organisationer. Genom att acceptera en organisation kan alla användare med den e-postdomänen registrera sig UTAN e-postverifiering.",
|
||||
"unverified": "Overifierad",
|
||||
"dns_missing": "DNS saknas",
|
||||
"mark_dns_configured": "Markera som DNS konfigurerad",
|
||||
"value": "Värde",
|
||||
"your_organization_updated_sucessfully": "Din organisation har uppdaterats",
|
||||
"team_no_event_types": "Detta team har inga händelsetyper",
|
||||
"seat_options_doesnt_multiple_durations": "Platsalternativet stöder inte flera varaktigheter",
|
||||
"include_calendar_event": "Inkludera kalenderhändelse",
|
||||
"recently_added": "Nyligen tillagda",
|
||||
"no_members_found": "Inga medlemmar hittades",
|
||||
"event_setup_length_error": "Konfigurering av evenemang: Längden måste vara minst 1 minut.",
|
||||
"availability_schedules": "Scheman för tillgänglighet",
|
||||
"view_only_edit_availability_not_onboarded": "Den här användaren har inte slutfört introduktionen. Du kommer inte att kunna ange deras tillgänglighet förrän de har slutfört introduktionen.",
|
||||
"view_only_edit_availability": "Du tittar på användarens tillgänglighet. Du kan bara redigera din egen tillgänglighet.",
|
||||
"edit_users_availability": "Redigera användarens tillgänglighet: {{username}}",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -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,7 +8,8 @@ import prisma from "@calcom/prisma";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario";
|
||||
|
||||
import { getDate, getGoogleCalendarCredential, createBookingScenario } from "../utils/bookingScenario";
|
||||
|
||||
// TODO: Mock properly
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(null);
|
||||
@@ -16,7 +17,7 @@ prismaMock.user.findMany.mockResolvedValue([]);
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
WEBAPP_URL: "http://localhost:3000"
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
}));
|
||||
|
||||
declare global {
|
||||
@@ -143,7 +144,6 @@ const TestData = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const cleanup = async () => {
|
||||
await prisma.eventType.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
@@ -296,7 +296,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// getSchedule returns timeslots in GMT
|
||||
@@ -328,7 +328,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
@@ -393,7 +393,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||
@@ -429,7 +429,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
// `slotInterval` takes precedence over `length`
|
||||
// 4:30 is utc so it is 10:00 in IST
|
||||
@@ -493,7 +493,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -514,7 +514,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -578,7 +578,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||
@@ -652,7 +652,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||
@@ -710,7 +710,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
@@ -774,7 +774,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||
@@ -852,7 +852,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(thisUserAvailability).toHaveTimeSlots(
|
||||
@@ -950,7 +950,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
@@ -980,7 +980,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
@@ -1088,7 +1088,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||
@@ -1116,7 +1116,7 @@ describe("getSchedule", () => {
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
||||
@@ -1139,4 +1139,3 @@ describe("getSchedule", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -194,10 +194,17 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const statusIn = where.OR[0].status?.in || [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const userIdIn = where.OR[0].userId?.in || [];
|
||||
const firstConditionMatches =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
|
||||
statusIn.includes(booking.status) &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(booking.userId === where.OR[0].userId || userIdIn.includes(booking.userId));
|
||||
|
||||
// We return this booking if either condition is met
|
||||
return firstConditionMatches;
|
||||
|
||||
@@ -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.
|
||||
@@ -262,6 +265,7 @@ export default class EventManager {
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -337,26 +341,37 @@ export default class EventManager {
|
||||
private async createAllCalendarEvents(event: CalendarEvent) {
|
||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||
for (const destination of event.destinationCalendar) {
|
||||
// Since GCal pushes events to multiple calendars we only want to create one event per booking
|
||||
let gCalAdded = false;
|
||||
const destinationCalendars: DestinationCalendar[] = event.destinationCalendar.reduce(
|
||||
(destinationCals, cal) => {
|
||||
if (cal.integration === "google_calendar") {
|
||||
if (gCalAdded) {
|
||||
return destinationCals;
|
||||
} else {
|
||||
gCalAdded = true;
|
||||
destinationCals.push(cal);
|
||||
}
|
||||
} else {
|
||||
destinationCals.push(cal);
|
||||
}
|
||||
return destinationCals;
|
||||
},
|
||||
[] as DestinationCalendar[]
|
||||
);
|
||||
for (const destination of destinationCalendars) {
|
||||
if (destination.credentialId) {
|
||||
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
|
||||
if (!credential) {
|
||||
// Fetch credential from DB
|
||||
const credentialFromDB = await prisma.credential.findUnique({
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: destination.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||
if (credentialFromDB && credentialFromDB.appId) {
|
||||
credential = {
|
||||
appName: credentialFromDB?.app.slug ?? "",
|
||||
id: credentialFromDB.id,
|
||||
type: credentialFromDB.type,
|
||||
key: credentialFromDB.key,
|
||||
@@ -364,6 +379,7 @@ export default class EventManager {
|
||||
teamId: credentialFromDB.teamId,
|
||||
invalid: credentialFromDB.invalid,
|
||||
appId: credentialFromDB.appId,
|
||||
user: credentialFromDB.user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -415,7 +431,7 @@ export default class EventManager {
|
||||
* @private
|
||||
*/
|
||||
|
||||
private getVideoCredential(event: CalendarEvent): CredentialWithAppName | undefined {
|
||||
private getVideoCredential(event: CalendarEvent): CredentialPayload | undefined {
|
||||
if (!event.location) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -443,7 +459,7 @@ export default class EventManager {
|
||||
event.location +
|
||||
" because credential is missing for the app"
|
||||
);
|
||||
videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
|
||||
videoCredential = { ...FAKE_DAILY_CREDENTIAL };
|
||||
}
|
||||
|
||||
return videoCredential;
|
||||
@@ -523,20 +539,13 @@ export default class EventManager {
|
||||
if (!credential) {
|
||||
// Fetch credential from DB
|
||||
const credentialFromDB = await prisma.credential.findUnique({
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||
if (credentialFromDB && credentialFromDB.appId) {
|
||||
credential = {
|
||||
appName: credentialFromDB?.app.slug ?? "",
|
||||
id: credentialFromDB.id,
|
||||
type: credentialFromDB.type,
|
||||
key: credentialFromDB.key,
|
||||
@@ -544,6 +553,7 @@ export default class EventManager {
|
||||
teamId: credentialFromDB.teamId,
|
||||
invalid: credentialFromDB.invalid,
|
||||
appId: credentialFromDB.appId,
|
||||
user: credentialFromDB.user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -566,6 +576,7 @@ export default class EventManager {
|
||||
where: {
|
||||
id: oldCalendarEvent.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
const calendar = await getCalendar(calendarCredential);
|
||||
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
|
||||
@@ -581,7 +592,7 @@ export default class EventManager {
|
||||
|
||||
if (!calendarReference) {
|
||||
return {
|
||||
appName: cred.appName,
|
||||
appName: cred.appId || "",
|
||||
type: cred.type,
|
||||
success: false,
|
||||
uid: "",
|
||||
|
||||
@@ -57,8 +57,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
|
||||
if (variable === bookingField) {
|
||||
let fieldValue;
|
||||
if (eventNameObj.bookingFields) {
|
||||
fieldValue =
|
||||
eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields]?.toString();
|
||||
const field = eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields];
|
||||
if (field && typeof field === "object" && "value" in field) {
|
||||
fieldValue = field?.value?.toString();
|
||||
} else {
|
||||
fieldValue = field?.toString();
|
||||
}
|
||||
}
|
||||
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
||||
}
|
||||
|
||||
@@ -22,25 +22,33 @@ export const getAggregatedAvailability = (
|
||||
return mergeOverlappingDateRanges(availability);
|
||||
};
|
||||
|
||||
function isSameDay(date1: Date, date2: Date) {
|
||||
return (
|
||||
date1.getUTCFullYear() === date2.getUTCFullYear() &&
|
||||
date1.getUTCMonth() === date2.getUTCMonth() &&
|
||||
date1.getUTCDate() === date2.getUTCDate()
|
||||
);
|
||||
}
|
||||
|
||||
function mergeOverlappingDateRanges(dateRanges: DateRange[]) {
|
||||
const sortedDateRanges = dateRanges.sort((a, b) => a.start.diff(b.start)); //is it already sorted before?
|
||||
dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
|
||||
|
||||
const mergedDateRanges: DateRange[] = [];
|
||||
|
||||
let currentRange = sortedDateRanges[0];
|
||||
let currentRange = dateRanges[0];
|
||||
if (!currentRange) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let i = 1; i < sortedDateRanges.length; i++) {
|
||||
const nextRange = sortedDateRanges[i];
|
||||
for (let i = 1; i < dateRanges.length; i++) {
|
||||
const nextRange = dateRanges[i];
|
||||
if (
|
||||
currentRange.start.utc().format("DD MM YY") === nextRange.start.utc().format("DD MM YY") &&
|
||||
currentRange.end.isAfter(nextRange.start)
|
||||
isSameDay(currentRange.start.toDate(), nextRange.start.toDate()) &&
|
||||
currentRange.end.valueOf() > nextRange.start.valueOf()
|
||||
) {
|
||||
currentRange = {
|
||||
start: currentRange.start,
|
||||
end: currentRange.end.isAfter(nextRange.end) ? currentRange.end : nextRange.end,
|
||||
end: currentRange.end.valueOf() > nextRange.end.valueOf() ? currentRange.end : nextRange.end,
|
||||
};
|
||||
} else {
|
||||
mergedDateRanges.push(currentRange);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Credential } from "@prisma/client";
|
||||
import type { Booking, EventType } from "@prisma/client";
|
||||
|
||||
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -9,10 +9,12 @@ import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
export async function getBusyTimes(params: {
|
||||
credentials: Credential[];
|
||||
credentials: CredentialPayload[];
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
username: string;
|
||||
eventTypeId?: number;
|
||||
startTime: string;
|
||||
@@ -23,10 +25,22 @@ export async function getBusyTimes(params: {
|
||||
seatedEvent?: boolean;
|
||||
rescheduleUid?: string | null;
|
||||
duration?: number | null;
|
||||
currentBookings?:
|
||||
| (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
|
||||
eventType: Pick<
|
||||
EventType,
|
||||
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
|
||||
> | null;
|
||||
_count?: {
|
||||
seatsReferences: number;
|
||||
};
|
||||
})[]
|
||||
| null;
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
userId,
|
||||
userEmail,
|
||||
username,
|
||||
eventTypeId,
|
||||
startTime,
|
||||
@@ -38,6 +52,7 @@ export async function getBusyTimes(params: {
|
||||
rescheduleUid,
|
||||
duration,
|
||||
} = params;
|
||||
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
userId,
|
||||
@@ -45,15 +60,6 @@ export async function getBusyTimes(params: {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
})}`
|
||||
);
|
||||
// get user email for attendee checking.
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A user is considered busy within a given time period if there
|
||||
@@ -83,49 +89,56 @@ export async function getBusyTimes(params: {
|
||||
in: [BookingStatus.ACCEPTED],
|
||||
},
|
||||
};
|
||||
// Find bookings that block this user from hosting further bookings.
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
// User is primary host (individual events, or primary organizer)
|
||||
{
|
||||
...sharedQuery,
|
||||
userId,
|
||||
},
|
||||
// The current user has a different booking at this time he/she attends
|
||||
{
|
||||
...sharedQuery,
|
||||
attendees: {
|
||||
some: {
|
||||
email: user.email,
|
||||
|
||||
// INFO: Refactored to allow this method to take in a list of current bookings for the user.
|
||||
// Will keep support for retrieving a user's bookings if the caller does not already supply them.
|
||||
// This function is called from multiple places but we aren't refactoring all of them at this moment
|
||||
// to avoid potential side effects.
|
||||
const bookings = params.currentBookings
|
||||
? params.currentBookings
|
||||
: await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
// User is primary host (individual events, or primary organizer)
|
||||
{
|
||||
...sharedQuery,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
// The current user has a different booking at this time he/she attends
|
||||
{
|
||||
...sharedQuery,
|
||||
attendees: {
|
||||
some: {
|
||||
email: userEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
afterEventBuffer: true,
|
||||
beforeEventBuffer: true,
|
||||
seatsPerTimeSlot: true,
|
||||
},
|
||||
},
|
||||
...(seatedEvent && {
|
||||
_count: {
|
||||
select: {
|
||||
seatsReferences: true,
|
||||
uid: true,
|
||||
userId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
afterEventBuffer: true,
|
||||
beforeEventBuffer: true,
|
||||
seatsPerTimeSlot: true,
|
||||
},
|
||||
},
|
||||
...(seatedEvent && {
|
||||
_count: {
|
||||
select: {
|
||||
seatsReferences: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const bookingSeatCountMap: { [x: string]: number } = {};
|
||||
const busyTimes = bookings.reduce(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { Booking, Prisma, EventType as PrismaEventType } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
@@ -14,6 +14,7 @@ import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { EventTypeMetaDataSchema, stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
import type {
|
||||
EventBusyDate,
|
||||
@@ -81,7 +82,9 @@ const getUser = (where: Prisma.UserWhereInput) =>
|
||||
where,
|
||||
select: {
|
||||
...availabilityUserSelect,
|
||||
credentials: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,6 +131,15 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
||||
eventType?: EventType;
|
||||
currentSeats?: CurrentSeats;
|
||||
rescheduleUid?: string | null;
|
||||
currentBookings?: (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
|
||||
eventType: Pick<
|
||||
PrismaEventType,
|
||||
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
|
||||
> | null;
|
||||
_count?: {
|
||||
seatsReferences: number;
|
||||
};
|
||||
})[];
|
||||
}
|
||||
) {
|
||||
const { username, userId, dateFrom, dateTo, eventTypeId, afterEventBuffer, beforeEventBuffer, duration } =
|
||||
@@ -180,6 +192,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
||||
endTime: getBusyTimesEnd,
|
||||
eventTypeId,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
username: `${user.username}`,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -187,6 +200,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
||||
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
||||
rescheduleUid: initialData?.rescheduleUid || null,
|
||||
duration,
|
||||
currentBookings: initialData?.currentBookings,
|
||||
});
|
||||
|
||||
const detailedBusyTimes: EventBusyDetails[] = [
|
||||
|
||||
@@ -9,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'
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function getServerSession(options: {
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
impersonatedByUID: token.impersonatedByUID ?? undefined,
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
organizationId: token.organizationId,
|
||||
org: token.org,
|
||||
locale: user.locale ?? undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||
import { getOrgFullDomain, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -17,7 +18,7 @@ import { isENVDev } from "@calcom/lib/env";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { ErrorCode } from "./ErrorCode";
|
||||
@@ -30,6 +31,8 @@ const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
|
||||
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
|
||||
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
|
||||
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
|
||||
const ORGANIZATIONS_AUTOLINK =
|
||||
process.env.ORGANIZATIONS_AUTOLINK === "1" || process.env.ORGANIZATIONS_AUTOLINK === "true";
|
||||
|
||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||
|
||||
@@ -53,6 +56,33 @@ export const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T) =>
|
||||
return metadata.success && metadata.data?.subscriptionId;
|
||||
});
|
||||
|
||||
const checkIfUserShouldBelongToOrg = async (idP: IdentityProvider, email: string) => {
|
||||
const [orgUsername, apexDomain] = email.split("@");
|
||||
if (!ORGANIZATIONS_AUTOLINK || idP !== "GOOGLE") return { orgUsername, orgId: undefined };
|
||||
const existingOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
metadata: {
|
||||
path: ["isOrganizationVerified"],
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
path: ["orgAutoAcceptEmail"],
|
||||
equals: apexDomain,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
return { orgUsername, orgId: existingOrg?.id };
|
||||
};
|
||||
|
||||
const providers: Provider[] = [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
@@ -402,7 +432,14 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
role: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
@@ -419,12 +456,23 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
|
||||
// Check if the existingUser has any active teams
|
||||
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
|
||||
const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser;
|
||||
const { teams: _teams, organization, ...existingUserWithoutTeamsField } = existingUser;
|
||||
|
||||
const parsedOrgMetadata = teamMetadataSchema.parse(organization?.metadata ?? {});
|
||||
|
||||
return {
|
||||
...existingUserWithoutTeamsField,
|
||||
...token,
|
||||
belongsToActiveTeam,
|
||||
org: organization
|
||||
? {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "",
|
||||
fullDomain: getOrgFullDomain(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""),
|
||||
domainSuffix: subdomainSuffix(),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
if (!user) {
|
||||
@@ -448,7 +496,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
role: user.role,
|
||||
impersonatedByUID: user?.impersonatedByUID,
|
||||
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||
organizationId: user?.organizationId,
|
||||
org: user?.org,
|
||||
locale: user?.locale,
|
||||
};
|
||||
}
|
||||
@@ -487,7 +535,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
role: existingUser.role,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
organizationId: token?.organizationId,
|
||||
org: token?.org,
|
||||
locale: existingUser.locale,
|
||||
};
|
||||
}
|
||||
@@ -507,7 +555,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
role: token.role as UserPermissionRole,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
organizationId: token?.organizationId,
|
||||
org: token?.org,
|
||||
locale: token.locale,
|
||||
},
|
||||
};
|
||||
@@ -716,16 +764,26 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
return "/auth/error?error=use-identity-login";
|
||||
}
|
||||
|
||||
// Associate with organization if enabled by flag and idP is Google (for now)
|
||||
const { orgUsername, orgId } = await checkIfUserShouldBelongToOrg(idP, user.email);
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
// Slugify the incoming name and append a few random characters to
|
||||
// prevent conflicts for users with the same name.
|
||||
username: usernameSlug(user.name),
|
||||
username: orgId ? slugify(orgUsername) : usernameSlug(user.name),
|
||||
emailVerified: new Date(Date.now()),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
identityProvider: idP,
|
||||
identityProviderId: account.providerAccountId,
|
||||
...(orgId && {
|
||||
verified: true,
|
||||
organization: { connect: { id: orgId } },
|
||||
teams: {
|
||||
create: { role: MembershipRole.MEMBER, accepted: true, team: { connect: { id: orgId } } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -327,6 +327,7 @@ const BookerComponent = ({
|
||||
prefetchNextMonth={prefetchNextMonth}
|
||||
monthCount={monthCount}
|
||||
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
||||
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
|
||||
/>
|
||||
</BookerSection>
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -19,6 +19,7 @@ type AvailableTimeSlotsProps = {
|
||||
prefetchNextMonth: boolean;
|
||||
monthCount: number | undefined;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,7 @@ export const AvailableTimeSlots = ({
|
||||
extraDays,
|
||||
limitHeight,
|
||||
seatsPerTimeSlot,
|
||||
showAvailableSeatsCount,
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
}: AvailableTimeSlotsProps) => {
|
||||
@@ -60,6 +62,7 @@ export const AvailableTimeSlots = ({
|
||||
seatsPerTimeSlot,
|
||||
attendees,
|
||||
bookingUid,
|
||||
showAvailableSeatsCount,
|
||||
});
|
||||
|
||||
if (seatsPerTimeSlot && seatsPerTimeSlot - attendees > 1) {
|
||||
@@ -116,6 +119,7 @@ export const AvailableTimeSlots = ({
|
||||
date={dayjs(slots.date)}
|
||||
slots={slots.slots}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
showAvailableSeatsCount={showAvailableSeatsCount}
|
||||
availableMonth={
|
||||
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
|
||||
? dayjs(slots.date).format("MMM")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
|
||||
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
|
||||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -130,13 +131,12 @@ export const EventMeta = () => {
|
||||
<EventMetaBlock icon={User} className={`${colorClass}`}>
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<p>
|
||||
{bookingSeatAttendeesQty ? eventTotalSeats - bookingSeatAttendeesQty : eventTotalSeats} /{" "}
|
||||
{eventTotalSeats}{" "}
|
||||
{t("seats_available", {
|
||||
count: bookingSeatAttendeesQty
|
||||
? eventTotalSeats - bookingSeatAttendeesQty
|
||||
: eventTotalSeats,
|
||||
})}
|
||||
<SeatsAvailabilityText
|
||||
showExact={!!seatedEventData.showAvailableSeatsCount}
|
||||
totalSeats={eventTotalSeats}
|
||||
bookedSeats={bookingSeatAttendeesQty || 0}
|
||||
variant="fraction"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EventMetaBlock>
|
||||
|
||||
@@ -33,6 +33,7 @@ type SeatedEventData = {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
attendees?: number;
|
||||
bookingUid?: string;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
};
|
||||
|
||||
export type BookerStore = {
|
||||
@@ -206,6 +207,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
||||
seatsPerTimeSlot: undefined,
|
||||
attendees: undefined,
|
||||
bookingUid: undefined,
|
||||
showAvailableSeatsCount: true,
|
||||
},
|
||||
setSeatedEventData: (seatedEventData: SeatedEventData) => {
|
||||
set({ seatedEventData });
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Button, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { useBookerStore } from "../Booker/store";
|
||||
import { useTimePreferences } from "../lib";
|
||||
import { SeatsAvailabilityText } from "./SeatsAvailabilityText";
|
||||
import { TimeFormatToggle } from "./TimeFormatToggle";
|
||||
|
||||
type AvailableTimesProps = {
|
||||
@@ -24,6 +25,7 @@ type AvailableTimesProps = {
|
||||
bookingUid?: string
|
||||
) => void;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showAvailableSeatsCount?: boolean | null;
|
||||
showTimeFormatToggle?: boolean;
|
||||
className?: string;
|
||||
availableMonth?: string | undefined;
|
||||
@@ -35,6 +37,7 @@ export const AvailableTimes = ({
|
||||
slots,
|
||||
onTimeSelect,
|
||||
seatsPerTimeSlot,
|
||||
showAvailableSeatsCount,
|
||||
showTimeFormatToggle = true,
|
||||
className,
|
||||
availableMonth,
|
||||
@@ -110,15 +113,16 @@ export const AvailableTimes = ({
|
||||
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
|
||||
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
|
||||
{hasTimeSlots && !bookingFull && (
|
||||
<p className="flex items-center text-sm lowercase">
|
||||
<p className="flex items-center text-sm">
|
||||
<span
|
||||
className={classNames(colorClass, "mr-1 inline-block h-2 w-2 rounded-full")}
|
||||
aria-hidden
|
||||
/>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot}{" "}
|
||||
{t("seats_available", {
|
||||
count: slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot,
|
||||
})}
|
||||
<SeatsAvailabilityText
|
||||
showExact={!!showAvailableSeatsCount}
|
||||
totalSeats={seatsPerTimeSlot}
|
||||
bookedSeats={slot.attendees || 0}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Whether to show the exact number of seats available or not
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showExact: boolean;
|
||||
/**
|
||||
* Shows available seats count as either whole number or fraction.
|
||||
*
|
||||
* Applies only when `showExact` is `true`
|
||||
*
|
||||
* @default "whole"
|
||||
*/
|
||||
variant?: "whole" | "fraction";
|
||||
/** Number of seats booked in the event */
|
||||
bookedSeats: number;
|
||||
/** Total number of seats in the event */
|
||||
totalSeats: number;
|
||||
};
|
||||
|
||||
export const SeatsAvailabilityText = ({
|
||||
showExact = true,
|
||||
bookedSeats,
|
||||
totalSeats,
|
||||
variant = "whole",
|
||||
}: Props) => {
|
||||
const { t } = useLocale();
|
||||
const availableSeats = totalSeats - bookedSeats;
|
||||
const isHalfFull = bookedSeats / totalSeats >= 0.5;
|
||||
const isNearlyFull = bookedSeats / totalSeats >= 0.83;
|
||||
|
||||
return (
|
||||
<span className={classNames(showExact && "lowercase")}>
|
||||
{showExact
|
||||
? `${availableSeats}${variant === "fraction" ? ` / ${totalSeats}` : ""} ${t("seats_available", {
|
||||
count: availableSeats,
|
||||
})}`
|
||||
: isNearlyFull
|
||||
? t("seats_nearly_full")
|
||||
: isHalfFull
|
||||
? t("seats_half_full")
|
||||
: t("seats_available", {
|
||||
count: availableSeats,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,10 @@ function RenderIcon({
|
||||
return (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className="me-[10px] h-4 w-4 opacity-70 invert-[.65] dark:invert-0"
|
||||
className={classNames(
|
||||
eventLocationType?.iconUrl?.includes("-dark") && "dark:invert",
|
||||
"me-[10px] h-4 w-4"
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import { Title, CustomArgsTable } from "@calcom/storybook/components";
|
||||
|
||||
import { StorybookTrpcProvider } from "../../../ui/components/mocks/trpc.tsx";
|
||||
import { VerifyCodeDialog } from "./VerifyCodeDialog";
|
||||
|
||||
<Meta title="Features/VerifyCodeDialog" component={VerifyCodeDialog} />
|
||||
|
||||
<Title title="VerifyCodeDialog" suffix="Brief" subtitle="Version 1.0 — Last Update: 11 Sep 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `VerifyCodeDialog` component allows users to enter a verification code sent to their email. The component provides feedback in case of an error and can handle different verification processes depending on whether the user session is required.
|
||||
|
||||
## Structure
|
||||
|
||||
This component contains an input form to capture a 6-digit verification code, error handling UI, and action buttons.
|
||||
|
||||
<CustomArgsTable of={VerifyCodeDialog} />
|
||||
|
||||
## VerifyCodeDialog Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="VerifyCodeDialog"
|
||||
args={{
|
||||
isOpenDialog: true,
|
||||
setIsOpenDialog: () => {},
|
||||
email: "example@email.com",
|
||||
onSuccess: () => {},
|
||||
onError: (err) => {
|
||||
if (err.message === "invalid_code") {
|
||||
alert("Code provided is invalid");
|
||||
}
|
||||
},
|
||||
}}
|
||||
argTypes={{
|
||||
isOpenDialog: { control: "boolean", description: "Indicates whether the dialog is open or not." },
|
||||
setIsOpenDialog: { action: "setIsOpenDialog", description: "Function to set the dialog state." },
|
||||
email: { control: "text", description: "Email to which the verification code was sent." },
|
||||
onSuccess: { action: "onSuccess", description: "Callback function when verification succeeds." },
|
||||
onError: { action: "onError", description: "Callback function when verification fails." },
|
||||
isUserSessionRequiredToVerify: {
|
||||
control: "boolean",
|
||||
description: "Indicates if user session is required for verification.",
|
||||
},
|
||||
}}>
|
||||
{(args) => (
|
||||
<StorybookTrpcProvider>
|
||||
<VerifyCodeDialog {...args} />
|
||||
</StorybookTrpcProvider>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
@@ -26,7 +26,8 @@ import { handleRefundError } from "@calcom/lib/payment/handleRefundError";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, MembershipRole, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
||||
@@ -44,7 +45,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: true, // Not leaking at the moment, be careful with
|
||||
credentials: { select: credentialForCalendarServiceSelect }, // Not leaking at the moment, be careful with
|
||||
email: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
@@ -434,6 +435,7 @@ async function handler(req: CustomRequest) {
|
||||
where: {
|
||||
id: credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
if (foundCalendarCredential) {
|
||||
calendarCredential = foundCalendarCredential;
|
||||
@@ -725,6 +727,7 @@ async function handleSeatedEventCancellation(
|
||||
where: {
|
||||
id: reference.credentialId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (credential) {
|
||||
@@ -733,13 +736,7 @@ async function handleSeatedEventCancellation(
|
||||
attendees: evt.attendees.filter((evtAttendee) => attendee.email !== evtAttendee.email),
|
||||
};
|
||||
if (reference.type.includes("_video")) {
|
||||
integrationsToUpdate.push(
|
||||
updateMeeting(
|
||||
{ ...credential, appName: evt.location?.replace("integrations:", "") || "" },
|
||||
updatedEvt,
|
||||
reference
|
||||
)
|
||||
);
|
||||
integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference));
|
||||
}
|
||||
if (reference.type.includes("_calendar")) {
|
||||
const calendar = await getCalendar(credential);
|
||||
|
||||
@@ -470,6 +470,128 @@ describe.sequential("handleNewBooking", () => {
|
||||
},
|
||||
timeout
|
||||
);
|
||||
test(
|
||||
`should create a successful booking when location is provided as label of an option(Done for Organizer Address)
|
||||
1. Should create a booking in the database
|
||||
2. Should send emails to the booker as well as organizer
|
||||
3. Should trigger BOOKING_CREATED webhook
|
||||
`,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
webhooks: [
|
||||
{
|
||||
userId: organizer.id,
|
||||
eventTriggers: ["BOOKING_CREATED"],
|
||||
subscriberUrl: "http://my-webhook.example.com",
|
||||
active: true,
|
||||
eventTypeId: 1,
|
||||
appId: null,
|
||||
},
|
||||
],
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
});
|
||||
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar");
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
expect(createdBooking).toContain({
|
||||
location: "New York",
|
||||
});
|
||||
|
||||
expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: mockBookingData.eventTypeId,
|
||||
},
|
||||
},
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered();
|
||||
|
||||
const testEmails = emails.get();
|
||||
expect(testEmails[0]).toHaveEmail({
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
to: `${organizer.email}`,
|
||||
});
|
||||
expect(testEmails[1]).toHaveEmail({
|
||||
htmlToContain: "<title>confirmed_event_type_subject</title>",
|
||||
to: `${booker.name} <${booker.email}>`,
|
||||
});
|
||||
expect(testEmails[1].html).toContain("<title>confirmed_event_type_subject</title>");
|
||||
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
|
||||
triggerEvent: "BOOKING_CREATED",
|
||||
payload: {
|
||||
metadata: {
|
||||
},
|
||||
responses: {
|
||||
name: { label: "your_name", value: "Booker" },
|
||||
email: { label: "email_address", value: "booker@example.com" },
|
||||
location: {
|
||||
label: "location",
|
||||
value: { optionValue: "", value: "New York" },
|
||||
},
|
||||
title: { label: "what_is_this_meeting_about" },
|
||||
notes: { label: "additional_notes" },
|
||||
guests: { label: "additional_guests" },
|
||||
rescheduleReason: { label: "reason_for_reschedule" },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user